SpringBoot JAR 是如何运行的

1、引言

在 SpringBoot 项目的根目录下执行mvn clean package,maven 会将当前的项目打成可执行的 jar 包,随后通过java -jar xxx.jar命令,即可启动运行 SpringBoot 服务,那SpringBoot的可执行jar包是如何运行的呢?

2、SpringBoot FAT JAR 结构

SpringBoot 的可执行 jar 包又称作“fat jar”,是包含所有三方依赖的jar。它与传统jar包最大的不同是包含了一个 lib目录和内嵌了 web 容器,SpringBoot fat jar 通常是由集成在 pom.xml 文件中的 maven 插件来生成的。配置在 pom 文件 build 元素中的 plugins 内:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
复制代码

执行mvn clean package后,进入项目根目录下target/目录,发现多了这两个文件xxx.jarxxx.jar.original,同时,xxx.jar的大小要远大于xxx.jar.original。其实,xxx.jar.original是属于原始 Maven 打包 jar 文件,而xxx.jar则是运行spring-boot-maven-plugin插件后,在xxx.jar.original的基础上引入了第三方依赖后repackage而成的。 spring-boot-maven-plugin默认有5 个goals:repackage、run、start、 stop、 build-info。在打包的时候默认使用的是repackagerepackage 能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为 xxx.original。 我们可以通过将xxx.jar可执行jar解包之后看下文件的结构,jdk提供了jar -xvf xxx.jar工具解压jar包;Fat JAR 采用 zip 压缩格式存储,因此凡是能解压 zip 压缩文件的软件,均可将 JAR 包解压,这里我们使用unzip解压: image.png image.png

解压后有三个目录:BOOT-INFMETA-INForg,文件分类用途如下:

  • BOOT-INF/classes 目录存放应用编译后的 class 文件,业务代码
  • BOOT-INF/lib 目录存放应用依赖的 JAR 包
  • META-INF/目录存放应用相关的元信息,如 MANIFEST.MF 文件
  • org/目录存放 Spring Boot 相关的 class 文件
3、 SpringBoot FAT JAR 启动

当 SpringBoot 应用可执行 JAR 文件被java -jar命令执行时,其命令本身对 JAR 文件是否来自 SpringBoot 插件打包并不感知。换言之,该命令引导的是标准可执行 JAR 文件,而按照 Java 官方文档的规定,java -jar命令引导的具体启动类必须配置在MANIFEST.MF资源的Main-Class属性中: image.png 发现Main-Class属性指向org.springframework.boot.loader.JarLauncher,也就是说SpringBoot FATJAR 包的启动类并是不我们项目的主配置类,而是JarLauncher,项目的主配置类被定义成了Start-Class;这时候我们可以大胆猜测了,java -jar xxx.jar启动了JarLauncher启动类,JarLauncher执行了引导类(Start-Class)的main方法,从而启动了SpringBoot项目,接下来就是小心求证这一猜想。

通过上述解压 jar 包的方法,查看xxx.jar.original内的文件可以看出,org.springframework.boot.loader.JarLauncher并非项目中的文件,而是spring-boot-maven-plugin插件repackage进来的。 通过 search.maven.org 搜索JarLauncher的依赖坐标: image.png

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-loader</artifactId>
  <version>2.0.2.RELEASE</version>
</dependency>
复制代码

将该依赖坐标加到我们的项目pom的dependences中,这样就能查看对应源码了,后面分析spring-boot-loader原理。

既然JarLauncher就在打包后的 FAT JAR 中,就可以在解压后的根目录中,直接使用 java 命令引导 JarLauncher 类文件,同样可以启动 Springboot 项目,其实java -jar也是做了一样的事情: image.png

4、JarLauncher 原理
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);
    }
    
}
复制代码

Launcher 类的具体实现类有3个:JarLauncher、WarLauncher 和 PropertiesLauncher,这里以JarLauncher 为例来解析说明Spring Boot 基于 Launcher 来实现的启动过程。 程序入口为main函数中的new JarLauncher().launch(args);调用new JarLauncher()构造函数会触发其父类的构造函数调用ExecutableArchiveLauncher()调用:

public ExecutableArchiveLauncher() {
    try {
        this.archive = createArchive();
    }
    catch (Exception ex) {
        throw new IllegalStateException(ex);
    }
}
复制代码
/*
在 createArchive 方法中,根据当前类信息获得当前归档文件的路径(即打包后生成的可执行的 xxx.jar),
并检查文件路径是否存在,如果存在且是文件夹,则创建 ExplodedArchive 的对象,否则创建 JarFileArchive 的对象。
*/
protected final Archive createArchive() throws Exception {
    // 通过获得当前 class 类的信息,查找到当前归档文件的路径
    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);
    }
    // 如果是目录,则创建 ExplodedArchive,否则创建 JarFileArchive
    return (root.isDirectory() ? new ExplodedArchive(root)
            : new JarFileArchive(root));
}
复制代码

我们再回到 JarLauncher 的入口程序,当创建完 JarLauncher 对象,获得了当前归档文件的Archive,下一步便是调用launch方法,该方法由Launcher类实现。Launcher中的这个launch方法就是启动应用程序的入口,而该方法的定义是为了让子类的静态main方法调用的。

protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
    // 找到 /BOOT-INF/lib 下的 jar,及/BOOT-INF/classes 下所对应的archive,
    // 通过这些 archive 的 URL生成 LaunchedURLClassLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 获取 MANIFEST.MF 中的Start-Class(springboot项目的主配置类),使用创建的classLoader加载,并执行其 main 方法
    launch(args, getMainClass(), classLoader);
}
复制代码
protected String getMainClass() throws Exception {
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
        // 获取 MANIFEST.MF 中的Start-Class(springboot项目的主配置类)
        mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
        throw new IllegalStateException(
            "No 'Start-Class' manifest entry specified in " + this);
    }
    return mainClass;
}
复制代码

执行launch方法:

protected void launch(String[] args, String mainClass, ClassLoader classLoader)
    throws Exception {
    // 将classLoader(LaunchedURLClassLoader),设置为线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
    // 获取Start-Class(SpringBoot主配置类),并创建 MainMethodRunner 对象,调用其 run 方法。
    createMainMethodRunner(mainClass, args, classLoader).run();
}
复制代码

MainMethodRunner类的实现:

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 {
        // 使用线程上下文类加载器加载Start-Class(springboot项目的主配置类)
		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        // 反射调用 Start-Class 的main方法
		mainMethod.invoke(null, new Object[] { this.args });
	}
}
复制代码
5、总结

至此,springboot 可执行jar包的启动流程就结束了,接下去就是 SpringBoot 主配置类的main方法调用: SpringApplication.run(LearnSringBootApplication.class, args); 进入 SpringBoot 的启动流程,涉及到 spring 容器的启动和 SpringBoot 自动装配的相关内容,这将是一个更加复杂的过程啦!

猜你喜欢

转载自juejin.im/post/7129513801774268430