SpringBoot源碼解讀(一 .可執(zhí)行JAR源碼分析)

一.可執(zhí)行JAR結(jié)構(gòu)分析

在Spring Boot應(yīng)用中,使用spring-boot-maven-plugin插件執(zhí)行mvn package命令生成的jar文件,可以通過java -jar命令直接運(yùn)行,這種jar文件稱為可執(zhí)行jar文件(Executable JAR)。

1.可執(zhí)行jar文件的獲取

可以從任意SpringBoot工程中運(yùn)行mvn package命令生成的jar文件,如沒有現(xiàn)成的SpringBoot工程,可以參考下列步驟生成一個。
https://start.spring.io/中創(chuàng)建創(chuàng)建SpringBoot項目,填寫項目的Group、Aritact及Package信息如:

項目元數(shù)據(jù)信息

填寫完信息后,點(diǎn)擊GENERAT按鈕生成SpringBoot項目壓縮文件并下載到本地,通過unzip
命令解壓后,進(jìn)入項目目錄并執(zhí)行mvn package命令,在項目的target目錄下便生成了可執(zhí)行jar文件(executable-jar-0.0.1-SNAPSHOT.jar)和原始Maven打包的jar文件(executable-jar-0.0.1-SNAPSHOT.jar.original)等文件。接下來我們打開jar文件一窺究竟吧。

2.可執(zhí)行jar文件內(nèi)部結(jié)構(gòu)

執(zhí)行unzip executable-jar-0.0.1-SNAPSHOT.jar -d temp將jar包解壓到temp目錄下,在通過tree命令查看目錄結(jié)構(gòu):

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目錄存放應(yīng)用編譯后的class文件;
2.BOOT-INF/classpath.id 可執(zhí)行jar文件依賴的類路徑索引文件;
3.BOOT-INF/lib目錄存放應(yīng)用依賴的jar包;
4.META-INF目錄存放應(yīng)用相關(guān)的元信息,如MANIFEST.MF文件;
5.org目錄存放啟動SpringBoot相關(guān)的class文件;

通過解壓目錄看出,和傳統(tǒng)的jar文件相比,多了BOOT-INF目錄和啟動SpringBoot相關(guān)的class文件,并且將傳統(tǒng)的class文件放置到了BOOT-INF是classes目錄下,所依賴的jar均放到了BOOT-INF/lib目錄。
我們知道。通過java -jar運(yùn)行的是標(biāo)準(zhǔn)的可執(zhí)行jar文件,按照J(rèn)ava官方文檔的規(guī)定,該命令引導(dǎo)的具體啟動類必須配置在META-INF/MANIFEST.MF文件的Main-Class屬性中。那我們來查看一下該文件的內(nèi)容:

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

可以發(fā)現(xiàn)Main-Class屬性的值為org.springframework.boot.loader.JarLauncher,而我們自己的項目中的Main Class全路徑名(cn.lsp.springboot.executablejar.ExecutableJarApplication)則存放到了Start-Class屬性中。從文件內(nèi)容可以看出SpringBoot的運(yùn)行都是通過org.springframework.boot.loader.JarLauncher來引導(dǎo)的,該類就是可執(zhí)行jar的啟動器。

二.可執(zhí)行JAR源碼分析

由于可執(zhí)行jar文件的啟動類為org.springframework.boot.loader.JarLauncher,為了方便分析源碼了解其實現(xiàn)原理,我們將該類所在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);
    }

}

該類是一個標(biāo)準(zhǔn)的Java應(yīng)用程序入口類,繼承自ExecutableArchiveLauncher,常量DEFAULT_CLASSPATH_INDEX_LOCATION所指向的文件內(nèi)容為應(yīng)用依賴的jar文件類路徑。isNestedArchive方法用于判斷Archive.Entry是否是Jar文件中的資源,Archive.Entry有兩種實現(xiàn),JarFileArchive.JarFileEntryExplodedArchive.FileEntry,前者基于jar文件,后者基于文件系統(tǒng),所以JarLauncher支持Jar文件和文件系統(tǒng)兩種啟動方式。
當(dāng)執(zhí)行java -jar命令時,META-INF/MANIFEST.MF文件的Main-Class屬性將調(diào)用main(String[])方法,實際上是調(diào)用JarLauncher#launch(args)方法,該方法繼承于基類org.springframework.boot.loader.Launcher,他們之間的繼承層次圖如下:

org.springframework.boot.loader.Launcher
      org.springframework.boot.loader.ExecutableArchiveLauncher
            org.springframework.boot.loader.JarLauncher                  //用于引導(dǎo)jar文件
            org.springframework.boot.loader.WarLauncher                 // 用于引導(dǎo)war文件

下面分析Launcher#launch(args)方法實現(xiàn):

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系統(tǒng)屬性java.protocol.handler.pkgs中,即org.springframework.boot.loader.jar.Handler,其實現(xiàn)協(xié)議為JAR,用于覆蓋JDK內(nèi)建的sun.net.www.protocol.jar.Handler。由于SpringBoot的可執(zhí)行Jar文件除了包含傳統(tǒng)的Java Jar中的資源外,還包含依賴的Jar文件,當(dāng)SpringBoot的可執(zhí)行jar被java -jar命令引導(dǎo)時,其內(nèi)部的jar文件無法被JDK內(nèi)建的sun.net.www.protocol.jar.Handler當(dāng)做Class Path,所以需要替換才能確保正常運(yùn)行。

createClassLoader(Iterator)方法用于創(chuàng)建LaunchedURLClassLoader,實現(xiàn)類的加載。

最后調(diào)用實際的引導(dǎo)類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);
    }

該方法的實際執(zhí)行者為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對象需要關(guān)聯(lián)mainClass及main方法參數(shù)args,通過反射來調(diào)用項目中真正的入口類的main方法,即META-INF/MANIFEST.MF文件中指定的Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication。至此,應(yīng)用程序的class path等環(huán)境在啟動前已準(zhǔn)備完畢,真正進(jìn)入應(yīng)用的啟動階段。

三.總結(jié)

1.SpringBoot的Launcher有JarLauncher和WarLauncher,前者引導(dǎo)jar文件啟動,后者引導(dǎo)war文件啟動;
2.SpringBoot的Launcher有兩種引導(dǎo)模式,基于Jar和文件系統(tǒng);
3.由于SpringBoot生成的可執(zhí)行jar文件與傳統(tǒng)jar文件不同,因此需要實現(xiàn)自己的org.springframework.boot.loader.jar.Handler來覆蓋JDK內(nèi)建的sun.net.www.protocol.jar.Handler,從而按照SpringBoot自己的方式來初始化classpath等環(huán)境并引導(dǎo)jar運(yùn)行;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容