Programmatically starting a Spring Boot application

This piece explores launching a Spring Boot application from another Java program. Unlike traditional Java applications, Spring Boot applications are packaged as executable JAR archives, bundling all dependencies within as nested JARs. This self-contained structure simplifies distribution and deployment.

Building these executable JARs is typically managed by a maven plugin, resulting in a single, user-friendly file. Running the application is as simple as using the command java -jar mySpringProg.jar. This launches the application, displaying formatted information on the console. However, what if a developer wants to initiate a Spring Boot application from within another Java program automatically?

Understanding Nested JARs

To package a Java program and its dependencies into a single executable JAR, a technique called “shading” can be employed. Shading involves including and renaming dependencies, relocating classes, and rewriting bytecode and resources, essentially creating a bundled copy within the application’s code.

While shading works for simpler cases, it can lead to conflicts if multiple dependencies share resources or classes with identical names and paths. Spring Boot bypasses this issue by packaging dependency JARs as nested JARs within the main executable JAR.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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

Examining a JAR archive, we find a structure akin to a standard Java-runnable JAR. Spring Boot loader classes reside in the org/springframework/boot/loader path, while user classes and dependencies are organized under BOOT-INF/classes and BOOT-INF/lib, respectively.

Note: If you’re unfamiliar with Spring, our “Top 10 Most Common Spring Framework Mistakes” article might be a helpful resource.

A typical Spring Boot JAR comprises:

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

The Spring Boot Classloader prioritizes loading JAR libraries before project classes, leading to subtle differences when running a Spring Boot application from an IDE (Eclipse, IntelliJ) compared to running it from the console.

For a deeper dive into class overrides and classloaders, refer to this article.

Launching Spring Boot Applications

Manually launching a Spring Boot application via the command line or shell is straightforward:

1
java -jar example.jar

However, programmatic launching from another Java program requires a more involved approach. This involves loading the org/springframework/boot/loader/*.class code, using Java reflection to instantiate JarFileArchive and JarLauncher, and finally invoking the launch(String[]) method.

Let’s break down this process step-by-step.

Loading Spring Boot Loader Classes

As previously mentioned, a Spring Boot JAR file shares the structure of any standard JAR archive. This allows us to load org/springframework/boot/loader/*.class entries, creating Class objects for later use in launching our Spring Boot application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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);

In this code snippet, classMap stores Class objects mapped to their corresponding package names. For example, the String “org.springframework.boot.loader.JarLauncher” would map to the JarLauncher.class object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();

Upon completion of the while loop, we have a map populated with Spring Boot loader class objects.

Automating the Launch Process

With the loading phase complete, we can move on to automating the actual launch of our application.

Java reflection enables the creation of objects from our loaded classes, a feature we’ll leverage in this tutorial.

The initial step is to create a JarFileArchive object.

1
2
3
4
5
6
7
8
// 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 JarFileArchive constructor requires a File(String) object as an argument, which we need to provide.

Next, we create a JarLauncher object. Its constructor, in turn, expects an Archive object.

1
2
3
4
5
6
7
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);

It’s important to note that Archive is an interface, while JarFileArchive is a concrete implementation of that interface.

Finally, we invoke the launch(String[]) method on our newly created jarLauncher object.

1
2
3
4
5
6
7
8
// 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]});

This invoke(jarLauncer, new Object[]{new String[0]}) call effectively launches the Spring Boot application. The main thread will pause here, awaiting the Spring Boot application’s termination.

Insights into the Spring Boot Classloader

Inspecting the structure of our Spring Boot JAR file reveals the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
+--- 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
          \--- (...)

Notice the three distinct entry types:

  • 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 LaunchedURLClassLoader, residing at the root of the Spring Boot JAR application.

The LaunchedURLClassLoader loads class content (BOOT-INF/classes) after library content (BOOT-INF/lib), which differs from the IDE’s approach. For instance, Eclipse prioritizes class content over libraries in the classpath.

As an extension of java.net.URLClassLoader, LaunchedURLClassLoader is initialized with URLs pointing to resources like JAR archives or class folders. During class loading, these resources are searched sequentially in the order specified by their URLs, using the first matching resource encountered.

Conclusion

Traditional Java applications require explicit enumeration of all dependencies in the classpath, often leading to a cumbersome startup process.

In contrast, Spring Boot applications offer a streamlined approach. They manage dependencies internally, abstracting away this complexity from the end user.

However, launching a Spring Boot application programmatically from another Java program introduces its own set of complexities. It necessitates loading Spring Boot loader classes, creating specific objects like JarFileArchive and JarLauncher, and utilizing Java reflection to trigger the launch process.

In essence: Spring Boot excels at handling background tasks, freeing developers to concentrate on more valuable aspects of development, such as feature implementation and testing.

Licensed under CC BY-NC-SA 4.0