晓我课堂

SpringBoot源码解读(一 .可执行JAR源码分析)

2022-01-05  本文已影响0人  liushiping

一.可执行JAR结构分析

在Spring Boot应用中,使用spring-boot-maven-plugin插件执行mvn package命令生成的jar文件,可以通过java -jar命令直接运行,这种jar文件称为可执行jar文件(Executable JAR)。

1.可执行jar文件的获取

可以从任意SpringBoot工程中运行mvn package命令生成的jar文件,如没有现成的SpringBoot工程,可以参考下列步骤生成一个。
https://start.spring.io/中创建创建SpringBoot项目,填写项目的Group、Aritact及Package信息如:

项目元数据信息
填写完信息后,点击GENERAT按钮生成SpringBoot项目压缩文件并下载到本地,通过unzip
命令解压后,进入项目目录并执行mvn package命令,在项目的target目录下便生成了可执行jar文件(executable-jar-0.0.1-SNAPSHOT.jar)和原始Maven打包的jar文件(executable-jar-0.0.1-SNAPSHOT.jar.original)等文件。接下来我们打开jar文件一窥究竟吧。
2.可执行jar文件内部结构

执行unzip executable-jar-0.0.1-SNAPSHOT.jar -d temp将jar包解压到temp目录下,在通过tree命令查看目录结构:

william@liushipingdeMacBook-Pro target % tree temp 
temp
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── cn
│   │       └── lsp
│   │           └── springboot
│   │               └── executablejar
│   │                   └── ExecutableJarApplication.class
│   ├── classpath.idx
│   ├── layers.idx
│   └── lib
│       ├── ... ...
│       ├── spring-boot-2.6.2.jar
│       ├── ... ...
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── cn.lsp.springboot
│           └── executable-jar
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── ClassPathIndexFile.class
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── ... ...
1.BOOT-INF/classes目录存放应用编译后的class文件;
2.BOOT-INF/classpath.id 可执行jar文件依赖的类路径索引文件;
3.BOOT-INF/lib目录存放应用依赖的jar包;
4.META-INF目录存放应用相关的元信息,如MANIFEST.MF文件;
5.org目录存放启动SpringBoot相关的class文件;

通过解压目录看出,和传统的jar文件相比,多了BOOT-INF目录和启动SpringBoot相关的class文件,并且将传统的class文件放置到了BOOT-INF是classes目录下,所依赖的jar均放到了BOOT-INF/lib目录。
我们知道。通过java -jar运行的是标准的可执行jar文件,按照Java官方文档的规定,该命令引导的具体启动类必须配置在META-INF/MANIFEST.MF文件的Main-Class属性中。那我们来查看一下该文件的内容:

william@liushipingdeMacBook-Pro temp % cat META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 16
Implementation-Title: executable-jar
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication
Spring-Boot-Version: 2.6.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

可以发现Main-Class属性的值为org.springframework.boot.loader.JarLauncher,而我们自己的项目中的Main Class全路径名(cn.lsp.springboot.executablejar.ExecutableJarApplication)则存放到了Start-Class属性中。从文件内容可以看出SpringBoot的运行都是通过org.springframework.boot.loader.JarLauncher来引导的,该类就是可执行jar的启动器。

二.可执行JAR源码分析

由于可执行jar文件的启动类为org.springframework.boot.loader.JarLauncher,为了方便分析源码了解其实现原理,我们将该类所在jar包引入项目的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
    <scope>provided</scope>
</dependency>
启动流程源码解读
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 {
        new JarLauncher().launch(args);
    }

}

该类是一个标准的Java应用程序入口类,继承自ExecutableArchiveLauncher,常量DEFAULT_CLASSPATH_INDEX_LOCATION所指向的文件内容为应用依赖的jar文件类路径。isNestedArchive方法用于判断Archive.Entry是否是Jar文件中的资源,Archive.Entry有两种实现,JarFileArchive.JarFileEntryExplodedArchive.FileEntry,前者基于jar文件,后者基于文件系统,所以JarLauncher支持Jar文件和文件系统两种启动方式。
当执行java -jar命令时,META-INF/MANIFEST.MF文件的Main-Class属性将调用main(String[])方法,实际上是调用JarLauncher#launch(args)方法,该方法继承于基类org.springframework.boot.loader.Launcher,他们之间的继承层次图如下:

org.springframework.boot.loader.Launcher
      org.springframework.boot.loader.ExecutableArchiveLauncher
            org.springframework.boot.loader.JarLauncher                  //用于引导jar文件
            org.springframework.boot.loader.WarLauncher                 // 用于引导war文件

下面分析Launcher#launch(args)方法实现:

public abstract class Launcher {
       ... ...
    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);
    }
      ...  ...
}

JarFile.registerUrlProtocolHandler()方法将package org.springframework.boot.loader追加到Java系统属性java.protocol.handler.pkgs中,即org.springframework.boot.loader.jar.Handler,其实现协议为JAR,用于覆盖JDK内建的sun.net.www.protocol.jar.Handler。由于SpringBoot的可执行Jar文件除了包含传统的Java Jar中的资源外,还包含依赖的Jar文件,当SpringBoot的可执行jar被java -jar命令引导时,其内部的jar文件无法被JDK内建的sun.net.www.protocol.jar.Handler当做Class Path,所以需要替换才能确保正常运行。

createClassLoader(Iterator)方法用于创建LaunchedURLClassLoader,实现类的加载。

最后调用实际的引导类launch(args, launchClass, classLoader)

    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);
    }

该方法的实际执行者为MainMethodRunner#run()方法。

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 });
    }
}

MainMethodRunner对象需要关联mainClass及main方法参数args,通过反射来调用项目中真正的入口类的main方法,即META-INF/MANIFEST.MF文件中指定的Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication。至此,应用程序的class path等环境在启动前已准备完毕,真正进入应用的启动阶段。

三.总结

1.SpringBoot的Launcher有JarLauncher和WarLauncher,前者引导jar文件启动,后者引导war文件启动;
2.SpringBoot的Launcher有两种引导模式,基于Jar和文件系统;
3.由于SpringBoot生成的可执行jar文件与传统jar文件不同,因此需要实现自己的org.springframework.boot.loader.jar.Handler来覆盖JDK内建的sun.net.www.protocol.jar.Handler,从而按照SpringBoot自己的方式来初始化classpath等环境并引导jar运行;

上一篇下一篇

猜你喜欢

热点阅读