Interviewer: why can SpringBoot be started with Jar package?

Many beginners will be confused about how Spring Boot packages the application code and all dependencies into a separate Jar package, because after the traditional Java project is packaged into a Jar package, the dependencies need to be specified through the - classpath attribute before it can run. Let's analyze and explain the startup principle of SpringBoot today.

Spring Boot packaging plug-in

Spring Boot provides a maven project packaging plug-in called Spring Boot Maven plugin, as follows:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

You can easily make the Spring Boot project into a jar package. In this way, we no longer need to deploy Web server containers such as Tomcat and Jetty.

Let's take a look at the structure of Spring Boot after packaging. Open the target directory and find two jar packages:

Among them, springboot-0.0.1-snapshot Jar is a packaged plug-in provided through Spring Boot. It is printed into Fat Jar in a new format, including all dependencies;

And springboot-0.0.1-snapshot jar. Original is generated by Java's native packaging method, which only contains the content of the project itself.

Organizational structure of SpringBoot FatJar

After expanding the executable Jar of Spring Boot, the structure is as follows:

  • BOOT-INF Directory: contains our project code (classes directory) and required dependencies (lib directory);
  • META-INF Directory: through manifest MF file provides the metadata of jar package and declares the startup class of jar;
  • org.springframework.boot.loader: the loader code of Spring Boot, the magic source of Jar in Jar loading.

We can see that if the BOOT-INF directory is removed, it will be a very common and standard Jar package, including meta information and executable code, which is / meta-inf / mainfest MF specifies the startup meta information of Jar package, org springframework. boot. The loader performs the corresponding logical operation.

MAINFEST.MF meta information

The content of meta information is as follows:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springboot
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.listenvision.SpringbootApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.6
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

It is equivalent to a Properties configuration file, and each line is a configuration item. Let's focus on two configuration items:

  • Main class configuration item: the startup class of the jar package specified by Java. It is set here as the JarLauncher class of the Spring Boot loader project to start the Spring Boot application.
  • Start class configuration item: the main startup class specified by Spring Boot, which is set here as the Application class defined by us.
  • Spring boot classes configuration item: Specifies the entry for loading application classes.
  • Spring boot lib configuration item: Specifies the library on which the application depends to load.

Starting principle

The startup principle of Spring Boot is shown in the following figure:

Source code analysis

JarLauncher

JarLauncher class is a startup class for Spring Boot jar package. The complete class diagram is as follows:

The WarLauncher class is the startup class for the Spring Boot war package. Launch class org springframework. boot. loader. Jarlauncher does not introduce classes into the project, but is added to the repackage of the spring boot Maven plugin plug-in.

Next, let's take a look at the source code of JarLauncher, which is relatively simple, as shown in the figure below:

public class JarLauncher extends ExecutableArchiveLauncher {

    private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
    
    static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
        if (entry.isDirectory()) {
            return entry.getName().equals("BOOT-INF/classes/");
        }
        return entry.getName().startsWith("BOOT-INF/lib/");
    };
    
    public JarLauncher() {
    }
    
    protected JarLauncher(Archive archive) {
        super(archive);
    }
    
    @Override
    protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
        // Only needed for exploded archives, regular ones already have a defined order
        if (archive instanceof ExplodedArchive) {
            String location = getClassPathIndexFileLocation(archive);
            return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
        }
        return super.getClassPathIndex(archive);
    }
    
   
    private String getClassPathIndexFileLocation(Archive archive) throws IOException {
        Manifest manifest = archive.getManifest();
        Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
        String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
        return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
    }
    
    @Override
    protected boolean isPostProcessingClassPathArchives() {
        return false;
    }
    
    @Override
    protected boolean isSearchCandidate(Archive.Entry entry) {
        return entry.getName().startsWith("BOOT-INF/");
    }
    
    @Override
    protected boolean isNestedArchive(Archive.Entry entry) {
        return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
    }
    
    public static void main(String[] args) throws Exception {
        //Call the launch method defined by the base class Launcher
        new JarLauncher().launch(args);
    }
}

We mainly look at its main method, which calls the launch method defined by the base class Launcher, which is the parent class of ExecutableArchiveLauncher. Let's take a look at the source code of the Launcher base class:

Launcher

public abstract class Launcher {
    private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
    
    protected void launch(String[] args) throws Exception {
        if (!isExploded()) {
            JarFile.registerUrlProtocolHandler();
        }
        ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
        launch(args, launchClass, classLoader);
    }
    
    @Deprecated
    protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
        return createClassLoader(archives.iterator());
    }
    
    protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
        List<URL> urls = new ArrayList<>(50);
        while (archives.hasNext()) {
            urls.add(archives.next().getUrl());
        }
        return createClassLoader(urls.toArray(new URL[0]));
    }
    
    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
    }
    
    protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        createMainMethodRunner(launchClass, args, classLoader).run();
    }
    
    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, args);
    }
    protected abstract String getMainClass() throws Exception;
    
    protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
        return getClassPathArchives().iterator();
    }
    
    @Deprecated
    protected List<Archive> getClassPathArchives() throws Exception {
        throw new IllegalStateException("Unexpected call to getClassPathArchives()");
    }
    
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
        String path = (location != null) ? location.getSchemeSpecificPart() : null;
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException("Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
    }
    
    protected boolean isExploded() {
        return false;
    }
    
    protected Archive getArchive() {
        return null;
    }
}
  1. The launch method will first create a class loader, and then determine whether the jar is in manifest The jarmode attribute is set in the MF file.
  2. If it is not set, the value of launchClass is returned from getMainClass(). This method is implemented by the subclass PropertiesLauncher and returns manifest Start class attribute value configured in MF.
  3. Call the createMainMethodRunner method, build a MainMethodRunner object and call its run method.

PropertiesLauncher

@Override
protected String getMainClass() throws Exception {
    //Load manifest.xml in the target directory of the jar package Start class configuration in MF file and find the startup class of springboot
    String mainClass = getProperty(MAIN, "Start-Class");
    if (mainClass == null) {
        throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified");
    }
    return mainClass;
}

MainMethodRunner

The executor of the main method of the target class. At this time, the mainClassName is assigned as manifest The start class attribute value configured in MF, that is, com listenvision. After SpringbootApplication, the main method of SpringbootApplication is executed through reflection, so as to achieve the effect of starting Spring Boot.

public class MainMethodRunner {
    private final String mainClassName;
    private final String[] args;
    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args != null) ? args.clone() : null;
    }
    public void run() throws Exception {
        Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.setAccessible(true);
        mainMethod.invoke(null, new Object[] { this.args });
    }
}

summary

  1. The jar package is similar to a zip compressed file, except that it has one more meta-inf / manifest.jar file than the zip file MF file, which is automatically created when building jar packages.
  2. Spring Boot provides a plug-in Spring Boot Maven plugin, which is used to package the program into an executable jar package.
  3. Use Java jar to start the jar package of Spring Boot. The first called entry class is JarLauncher. After calling the Launcher's launch internally, build the MainMethodRunner object. Finally, call the main method of SpringbootApplication through reflection to achieve the startup effect.

Keywords: Java Spring Boot

Added by FramezArt on Thu, 24 Feb 2022 09:36:46 +0200