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

一.可执行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运行;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容