Spring Boot Fat Jar 启动原理

johnniang 2019年12月04日 136次浏览

误区

开始学习及使用 Spring Boot 的时候,通常都编写过以下代码:

package com.example.myapplication;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

接着通过 Gradle 或者 Maven 构建工具打包得到文件app.jar,然后输入以下命令启动:

java -jar app.jar

这里读者可能会误以为 JVM 是直接调用的 Application#main 函数,然后开始执行 SpringApplication.run(Application.class, args);

接下来就开始解释这为什么会是误解

分析

首先我们分析一下 java -jar 这个操作到底做了什么。

查询 java 命令手册之后,在描述区有这么一句:

By default, the first argument that is not an option of the java command is the fully qualified name of the class to be called.
If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.

重点在于最后一句:启动类必须指定在 Manifest 清单中的 Main-Class 头中。

当然,直接查看 -jar option 也能找打类似的描述:

-jar filename
    Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains
    a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that
    serves as your application's starting point.
    
    When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are
    ignored.

至此我们已经了解到执行 java -jar app.jar 的第一步是寻找 Manifest 清单中的 Main-Class 头,找到启动类之后才进行调用 #main 函数。

解压 app.jar

app.jar 是经过 Gradle 或者 Mavenspring boot plugin 插件特殊处理。解压结果如下:

app.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-BOOT-INF
    +-com
    |  +-example
    |     +-myapplication
    |        +-Application.class
    |        +-Other classes...
    +-lib
       +-dependency1.jar
       +-dependency2.jar

可以从以下链接找到 jar 文件的规格要求。
https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html

打开文件 META-INF/MANIFEST.MF,内容如下:

Manifest-Version: 1.0
Implementation-Title: Example Application
Implementation-Version: 1.1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.myapplication.Application
Spring-Boot-Version: 2.2.1.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

该文件指定 org.springframework.boot.loader.JarLauncherMain-Class,也就意味着通过 java -jar app.jar 命令运行后首先执行的应该是 JarLauncher#main

JarLauncher 文件可以在以下链接找到。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java

突破

JarLauncherspring boot loader 子模块中的一个类,内容如下:

package org.springframework.boot.loader;

import org.springframework.boot.loader.archive.Archive;

/**
 * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
 * included inside a {@code /BOOT-INF/lib} directory and that application classes are
 * included inside a {@code /BOOT-INF/classes} directory.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @since 1.0.0
 */
public class JarLauncher extends ExecutableArchiveLauncher {

	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

	static final String BOOT_INF_LIB = "BOOT-INF/lib/";

	public JarLauncher() {
	}

	protected JarLauncher(Archive archive) {
		super(archive);
	}

	@Override
	protected boolean isNestedArchive(Archive.Entry entry) {
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}
}

JarLauncher#main 中调用的 Launcher#launch(args) 实际上是调用父类 org.springframework.boot.loader.Launcher#launch(args) 方法。

org.springframework.boot.loader.Launcher 可以在以下链接中找到。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java

Launcher 的主要的方法如下:

/**
 * Launch the application. This method is the initial entry point that should be
 * called by a subclass {@code public static void main(String[] args)} method.
 * @param args the incoming arguments
 * @throws Exception if the application fails to launch
 */
protected void launch(String[] args) throws Exception {
	JarFile.registerUrlProtocolHandler();
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	launch(args, getMainClass(), classLoader);
}

/**
 * Launch the application given the archive file and a fully configured classloader.
 * @param args the incoming arguments
 * @param mainClass the main class to run
 * @param classLoader the classloader
 * @throws Exception if the launch fails
 */
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
	Thread.currentThread().setContextClassLoader(classLoader);
	createMainMethodRunner(mainClass, args, classLoader).run();
}

大致的思路如下:

  • 遍历 jar 包中的依赖(Nest Jar)和 classes,并创建一个 ClassLoader
  • 根据清单文件(META-INF/Manifest.MF)找到所指定的 Start-Class:启动类;
  • 将刚刚创建的 ClassLoader 设置到当前线程中。
  • 最后通过反射找到启动类main 函数,并调用该函数。

直到最后一步执行完成,才开始真正执行 [email protected] 函数,才正式进入 Spring Boot 的初始化。