Launch an App Programmatically: A Spring Boot Tutorial

0
45

This article will demonstrate how to start a Spring Boot application from another Java program. A Spring Boot application is typically built into a single executable JAR archive. It contains all dependencies inside, packaged as nested JARs.

Likewise, a Spring Boot project is usually built as an executable JAR file by a provided maven plugin that does all the dirty work. The result is a convenient, single JAR file that is easy to share with others, deploy on a server, and so on.

Starting a Spring Boot application is as easy as typing java -jar mySpringProg.jar, and the application will print on console some nicely formatted info messages.

But what if a Spring Boot developer wants to run an application from another Java program, without human intervention?

How Nested JARs Work

To pack a Java program with all dependencies into a single runnable JAR file, dependencies that are also JAR files have to be provided and somehow stored inside the final runnable JAR file.

“Shading” is one option. Shading dependencies is the process of including and renaming dependencies, relocating the classes, and rewriting affected bytecode and resources in order to create a copy that is bundled alongside with an application’s (project) own code.

Shading allows users to unpack all classes and resources from dependencies and pack them back into a runnable JAR file. This might work for simple scenarios, however, if two dependencies contain the same resource file or class with the exact same name and path, they will overlap and the program might not work.

Spring Boot takes a different approach and packs dependency JARs inside runnable JAR, as nested JARs.

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-BOOT-INF
    +-classes
    |  +-mycompany
    |     +-project
    |        +-YourClasses.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar

A JAR archive is organized as a standard Java-runnable JAR file. Spring Boot loader classes are located at org/springframework/boot/loader path, while user classes and dependencies are at BOOT-INF/classes and BOOT-INF/lib.

Note: If you’re new to Spring, you may also want to take a look at our Top 10 Most Common Spring Framework Mistakes article.

A typical Spring Boot JAR file contains three types of entries:

  • Project classes
  • Nested JAR libraries
  • Spring Boot loader classes

Spring Boot Classloader will first set JAR libraries in the classpath and then project classes, which makes a slight difference between running a Spring Boot application from IDE (Eclipse, IntelliJ) and from console.

For additional information on class overrides and the classloader, you can consult this article.

Launching Spring Boot Applications

Launching a Spring Boot application manually from the command line or shell is easy as typing the following:

java -jar example.jar

However, starting a Spring Boot application programmatically from another Java program requires more effort. It’s necessary to load the org/springframework/boot/loader/*.class code, use a bit of Java reflection to instantiate JarFileArchive, JarLauncher, and invoke the launch(String[]) method.

We will take a more detailed look at how this is accomplished in the following sections.

Loading Spring Boot Loader Classes

As we already pointed out, a Spring Boot JAR file is just like any JAR archive. It is possible to load org/springframework/boot/loader/*.class entries, create Class objects, and use them to launch Spring Boot applications later on.

import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
. . .

	public static void loadJar(final String pathToJar) throws IOException . . . {

		// Class name to Class object mapping.
		final Map<String, Class<?>> classMap = new HashMap<>();

		final JarFile jarFile = new JarFile(pathToJar);
		final Enumeration<JarEntry> jarEntryEnum = jarFile.entries();

		final URL[] urls = { new URL("jar:file:" + pathToJar + "!/") };
		final URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);

Here we can see classMap will hold Class objects mapped to their respective package names, e.g., String value org.springframework.boot.loader.JarLauncher will be mapped to the JarLauncher.class object.

while (jarEntryEnum.hasMoreElements()) {

	final JarEntry jarEntry = jarEntryEnum.nextElement();

	if (jarEntry.getName().startsWith("org/springframework/boot")
	&& jarEntry.getName().endsWith(".class") == true) {

	int endIndex = jarEntryName.lastIndexOf(".class");

	className = jarEntryName.substring(0, endIndex).replace('/', '.');

		try {

			final Class<?> loadedClass = urlClassLoader.loadClass(className);

				result.put(loadedClass.getName(), loadedClass);
			}
			catch (final ClassNotFoundException ex) {

			}
		}
	}

	jarFile.close();

The end result of the while loop is a map populated with Spring Boot loader class objects.

Automating the Actual Launch

With loading out of the way, we can proceed to finalize the automatic launch and use it to actually start our app.

Java reflection allows the creation of objects from loaded classes, which is quite useful in the context of our tutorial.

The first step is to create a JarFileArchive object.

// Create JarFileArchive(File) object, needed for JarLauncher.
final Class<?> jarFileArchiveClass = 					result.get("org.springframework.boot.loader.archive.JarFileArchive");

final Constructor<?> jarFileArchiveConstructor = 
	jarFileArchiveClass.getConstructor(File.class);

final Object jarFileArchive = 
		jarFileArchiveConstructor.newInstance(new File(pathToJar));

The constructor of the JarFileArchive object takes a File(String) object as an argument, so it must be provided.

The next step is to create a JarLauncher object, which requires Archive in its constructor.

final Class<?> archiveClass = 	result.get("org.springframework.boot.loader.archive.Archive");
				
// Create JarLauncher object using JarLauncher(Archive) constructor. 
final Constructor<?> jarLauncherConstructor = 		mainClass.getDeclaredConstructor(archiveClass);

jarLauncherConstructor.setAccessible(true);
final Object jarLauncher = jarLauncherConstructor.newInstance(jarFileArchive);

To avoid confusion, please note that Archive is actually an interface, while JarFileArchive is one of the implementations.

The last step in the process is to call the launch(String[]) method on our newly created jarLauncher object. This is relatively straightforward and requires just a few lines of code.

// Invoke JarLauncher#launch(String[]) method.
final Class<?> launcherClass = 	result.get("org.springframework.boot.loader.Launcher");

final Method launchMethod = 
	launcherClass.getDeclaredMethod("launch", String[].class);
launchMethod.setAccessible(true);
				
launchMethod.invoke(jarLauncher, new Object[]{new String[0]});

The invoke(jarLauncer, new Object[]{new String[0]}) method will finally start the Spring Boot application. Note that the main thread will stop and wait here for the Spring Boot application to terminate.

A Word About the Spring Boot Classloader

Examining our Spring Boot JAR file will reveal the following structure:

+--- mySpringApp1-0.0.1-SNAPSHOT.jar
     +--- META-INF
     +--- BOOT-INF
     |    +--- classes                            # 1 - project classes
     |    |     | 
     |    |     +--- com.example.mySpringApp1
     |    |          --- SpringBootLoaderApplication.class
     |    |
     |    +--- lib                                # 2 - nested jar libraries
     |          +--- javax.annotation-api-1.3.1
     |          +--- spring-boot-2.0.0.M7.jar     
     |          --- (...)
     |
     +--- org.springframework.boot.loader         # 3 - Spring Boot loader classes
          +--- JarLauncher.class
          +--- LaunchedURLClassLoader.class
          --- (...)

Note the three types of entries:

  • Project classes
  • Nested JAR libraries
  • Spring Boot loader classes

Both project classes (BOOT-INF/classes) and nested JARs (BOOT-INF/lib) are handled by the same class loader LaunchedURLClassLoader. This loader resides in the root of the Spring Boot JAR application.

The LaunchedURLClassLoader will load the class content (BOOT-INF/classes) after the library content (BOOT-INF/lib), which is different from the IDE. For example, Eclipse will first place class content in the classpath and then libraries (dependencies).

LaunchedURLClassLoader extends java.net.URLClassLoader, which is created with a set of URLs that will be used for class loading. The URL might point to a location like a JAR archive or classes folder. When performing class loading, all of the resources specified by URLs will be traversed in the order the URLs were provided, and the first resource containing the searched class will be used.

Wrapping Up

A classic Java application requires all dependencies to be enumerated in the classpath argument, making the startup procedure somewhat cumbersome and complicated.

In contrast, Spring Boot applications are handy and easy to start from the command line. They manage all dependencies, and the end user does not need to worry about the details.

However, starting a Spring Boot application from another Java program makes the procedure more complicated, as it requires loading Spring Boot’s loader classes, creating specialized objects such as JarFileArchive and JarLauncher, and then using Java reflection to invoke the launch method.

Bottom line: Spring Boot can take care of a lot of menial tasks under the hood, allowing developers to free up time and focus on more useful work such as creating new features, testing, and so on.

Original Source

This site uses Akismet to reduce spam. Learn how your comment data is processed.