头图

Many beginners will be confused, how does Spring Boot package the application code and all dependencies into an independent Jar package, because after the traditional Java project is packaged into a Jar package, you need to specify the dependencies through the -classpath attribute to be able to run. Today we will analyze and explain the startup principle of SpringBoot.

Spring Boot packaging plugin

Spring Boot provides a maven project packaging plugin named spring-boot-maven-plugin , as follows:

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

You can easily package the Spring Boot project into a jar package. This way we no longer need to deploy web server containers like Tomcat, Jetty, etc.

Let's take a look at what the packaged structure of Spring Boot looks like. When we open the target directory, we find that there are two jar packages:

image-20220224140411627

Among them, springboot-0.0.1-SNAPSHOT.jar is a packaged plug-in provided by Spring Boot, which is marked as 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, and only contains the content of the project itself.

Organizational structure of SpringBoot FatJar

The structure after we expand the executable Jar from Spring Boot is as follows:

image-20220224141034423

  • BOOT-INF directory: contains our project code (classes directory) and required dependencies (lib directory);
  • META-INF directory: The metadata of the Jar package is provided through the MANIFEST.MF file, and the startup class of the jar is declared;
  • org.springframework.boot.loader : Spring Boot's loader code, which implements 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 parts. Its /META-INF/MAINFEST.MF specifies the startup meta information of the Jar package, and org.springframework.boot.loader performs the corresponding logical operation.

MAINFEST.MF meta information

The content of the 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, 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, which is set to 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 to the Application class we defined here.
  • Spring-Boot-Classes configuration item: Specify the entry for loading application classes.
  • Spring-Boot-Lib configuration item: Specify the library to load application dependencies.

Startup principle

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

Source code analysis

JarLauncher

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

The WarLauncher class is the startup class for the Spring Boot war package. The startup class org.springframework.boot.loader.JarLauncher is not introduced into the project, but added by the spring-boot-maven-plugin plugin repackage.

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

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 {
        //调用基类 Launcher 定义的 launch 方法
        new JarLauncher().launch(args);
    }
}

It mainly depends on 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 first creates the class loader and then determines whether the jar has the jarmode attribute set in the MANIFEST.MF file.
  2. If not set, the value of launchClass is returned from getMainClass() , which is implemented by a subclass of PropertiesLauncher and returns the value of the Start-Class property configured in MANIFEST.MF.
  3. Call createMainMethodRunner method, construct a MainMethodRunner object and call its run method.

PropertiesLauncher

@Override
protected String getMainClass() throws Exception {
    //加载 jar包 target目录下的  MANIFEST.MF 文件中 Start-Class配置,找到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, the mainClassName at this time is assigned the value of the Start-Class attribute configured in MANIFEST.MF, which is com.listenvision.SpringbootApplication , and then the main method of SpringbootApplication is executed by 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 });
    }
}

Summarize

  1. The jar package is similar to the zip compressed file, except that there is one more META-INF/MANIFEST.MF file than the zip file, which is automatically created when the jar package is built.
  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 Spring Boot jar package. The first entry class to be called is JarLauncher , and the MainMethodRunner object is built after internally calling the launch of Launcher , and finally the main method of SpringbootApplication is called by reflection to achieve the startup effect.

初念初恋
175 声望17 粉丝