當(dāng)我們的執(zhí)行 java -jar xxx.jar 的時(shí)候底層到底做了什么?

大家都知道我們常用的 SpringBoot 項(xiàng)目最終在線上運(yùn)行的時(shí)候都是通過(guò)啟動(dòng) java -jar xxx.jar 命令來(lái)運(yùn)行的。

那你有沒(méi)有想過(guò)一個(gè)問(wèn)題,那就是當(dāng)我們執(zhí)行 java -jar 命令后,到底底層做了什么就啟動(dòng)了我們的 SpringBoot 應(yīng)用呢?

或者說(shuō)一個(gè) SpringBoot 的應(yīng)用到底是如何運(yùn)行起來(lái)的呢?今天阿粉就帶大家來(lái)看下。

認(rèn)識(shí) jar

在介紹 java -jar 運(yùn)行原理之前我們先看一下 jar 包里面都包含了哪些內(nèi)容,我們準(zhǔn)備一個(gè) SpringBoot 項(xiàng)目,通過(guò)在 https://start.spring.io/ 上我們可以快速創(chuàng)建一個(gè) SpringBoot 項(xiàng)目,下載一個(gè)對(duì)應(yīng)版本和報(bào)名的 zip 包。

下載后的項(xiàng)目我們?cè)?pom 依賴?yán)锩婵梢钥吹接腥缦乱蕾?,這個(gè)插件是我們構(gòu)建可執(zhí)行 jar 的前提,所以如果想要打包成一個(gè) jar 那必須在 pom 有增加這個(gè)插件,從 start.spring.io 上創(chuàng)建的項(xiàng)目默認(rèn)是會(huì)帶上這個(gè)插件的。

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

接下來(lái)我們執(zhí)行 mvn package,執(zhí)行完過(guò)后在項(xiàng)目的 target 目錄里面我們可以看到有如下兩個(gè) jar 包,我們分別把這兩個(gè) jar 解壓一下看看里面的內(nèi)容,.original 后綴的 jar 需要把后面的 .original 去掉就可以解壓了。jar 文件的解壓跟我們平常的 zip 解壓是一樣的,jar 文件采用的是 zip 壓縮格式存儲(chǔ),所以任何可以解壓 zip 文件的軟件都可以解壓 jar 文件。

解壓過(guò)后,我們對(duì)比兩種解壓文件,可以發(fā)現(xiàn),兩個(gè)文件夾中的內(nèi)容還是有很大區(qū)別的,如下所示,左側(cè)是 demo-jar-0.0.1-SNAPSHOT.jar 右側(cè)是對(duì)應(yīng)的 original jar。

其中有一些相同的文件夾和文件,比如 META-INF,application.properties 等,而且我們可以明顯的看到左側(cè)的壓縮包中有項(xiàng)目需要依賴的所有庫(kù)文件,存放于 lib 文件夾中。

所以我們可以大膽的猜測(cè),左側(cè)的壓縮包就是 spring-boot-maven-plugin 這個(gè)插件幫我們把依賴的庫(kù)以及相應(yīng)的文件調(diào)整了一下目錄結(jié)構(gòu)而生成的,事實(shí)其實(shí)也是如此。

java -jar 原理

首先我們要知道的是這個(gè) java -jar 不是什么新的東西,而是 java 本身就自帶的命令,而且java -jar 命令在執(zhí)行的時(shí)候,命令本身對(duì)于這個(gè) jar 是不是 SpringBoot 項(xiàng)目是不感知的,只要是符合 Java 標(biāo)準(zhǔn)規(guī)范的jar 都可以通過(guò)這個(gè)命令啟動(dòng)。

而在 Java 官方文檔顯示,當(dāng) -jar 參數(shù)存在的時(shí)候,jar 文件資源里面必須包含用 Main-Class 指定的一個(gè)啟動(dòng)類(lèi),而且同樣根據(jù)規(guī)范這個(gè)資源文件 MANIFEST.MF 必須放在 /META-INF/ 目錄下。對(duì)比我們上面解壓后的文件,可以看到在左側(cè)的資源文件 MANIFEST.MF 文件中有如圖所示的一行。

[圖片上傳失敗...(image-3db0a9-1670339533552)]

可以看到這里的 Main-Class 屬性配置的是 org.springframework.boot.loader.JarLauncher,而如果小伙伴更仔細(xì)一點(diǎn)的話,會(huì)發(fā)現(xiàn)我們項(xiàng)目的啟動(dòng)類(lèi)也在這個(gè)文件里面,是通過(guò) Start-Class 字段來(lái)表示的,Start-Class 這個(gè)屬性不是 Java 官方的屬性。

由此我們先大膽的猜測(cè)一下,當(dāng)我們?cè)趫?zhí)行java -jar 的時(shí)候,由于我們的 jar 里面存在 MANIFEST.MF 文件,并且其中包含了 Main-Class 屬性且配置了 org.springframework.boot.loader.JarLauncher 類(lèi),通過(guò)調(diào)用 JarLauncher 類(lèi)結(jié)合 Start-Class 屬性引導(dǎo)出我們項(xiàng)目的啟動(dòng)類(lèi)進(jìn)行啟動(dòng)。接下來(lái)我們就通過(guò)源碼來(lái)驗(yàn)證一下這個(gè)猜想。

因?yàn)?JarLauncher 類(lèi)是在 spring-boot-loader 模塊,所以我們?cè)?pom 文件中增加如下依賴,就可以下載源碼進(jìn)行跟蹤了。

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-loader</artifactId>
            <scope>provided</scope>
</dependency>

通過(guò)源碼我們可以看到 JarLauncher 類(lèi)的代碼如下

package org.springframework.boot.loader;

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

public class JarLauncher extends ExecutableArchiveLauncher {

    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 boolean isPostProcessingClassPathArchives() {
        return false;
    }

    @Override
    protected boolean isNestedArchive(Archive.Entry entry) {
        return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
    }

    @Override
    protected String getArchiveEntryPathPrefix() {
        return "BOOT-INF/";
    }

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

}

其中有兩個(gè)點(diǎn)我們可以關(guān)注一下,第一個(gè)是這個(gè)類(lèi)有一個(gè) main 方法,這也是為什么 java -jar 命令可以進(jìn)行引導(dǎo)的原因,畢竟 java 程序都是通過(guò) main 方法進(jìn)行運(yùn)行的。其次是這里面有兩個(gè)路徑 BOOT-INF/classes/BOOT-INF/lib/這兩個(gè)路徑正好是我們的源碼路徑和第三方依賴路徑。

JarLauncher 類(lèi)里面的 main() 方法主要是運(yùn)行 Launcher 里面的 launch() 方法,這幾個(gè)類(lèi)的關(guān)系圖如下所示

跟著代碼我們可以看到最終調(diào)用的是這個(gè) run() 方法

而這里的參數(shù) mainClasslaunchClass 都是通過(guò)通過(guò)下面的邏輯獲取的,都是通過(guò)資源文件里面的 Start-Class 來(lái)進(jìn)行獲取的,這里正是我們項(xiàng)目的啟動(dòng)類(lèi),由此可以看到我們上面的猜想是正確的。

擴(kuò)展

上面的類(lèi)圖當(dāng)中我們還可以看到除了有 JarLauncher 以外還有一個(gè) WarLauncher 類(lèi),確實(shí)我們的 SpringBoot 項(xiàng)目也是可以配置成 war 進(jìn)行部署的。我們只需要將打包插件里面的 jar 更換成 war 即可。大家可以自行嘗試重新打包解壓進(jìn)行分析,這里 war 包部署方式只研究學(xué)習(xí)就好了,SpringBoot 應(yīng)用還是盡量都使用 Jar 的方式進(jìn)行部署。

總結(jié)

通過(guò)上面的內(nèi)容我們知道了當(dāng)我們?cè)趫?zhí)行 java -jar 的時(shí)候,根據(jù) java 官方規(guī)范會(huì)引導(dǎo) jar 包里面 MANIFEST.MF 文件中的 Main-Class 屬性對(duì)應(yīng)的啟動(dòng)類(lèi),該啟動(dòng)類(lèi)中必須包含 main() 方法。

而對(duì)于我們 SpringBoot 項(xiàng)目構(gòu)建的 jar 包,除了 Main-Class 屬性外還會(huì)有一個(gè) Start-Class 屬性綁定的是我們項(xiàng)目的啟動(dòng)類(lèi),當(dāng)我們?cè)趫?zhí)行 java -jar 的時(shí)候優(yōu)先引導(dǎo)的是 org.springframework.boot.loader.JarLauncher#main 方法,該方法內(nèi)部會(huì)通過(guò)引導(dǎo) Start-Class 屬性來(lái)啟動(dòng)我們的應(yīng)用代碼。

通過(guò)上面的分析相比大家對(duì)于 SpringBoot 是如何通過(guò) java -jar 進(jìn)行啟動(dòng)了有了一個(gè)詳細(xì)的了解,下次再有人問(wèn)你 SpringBoot 項(xiàng)目是如何啟動(dòng)的,請(qǐng)把這篇文章轉(zhuǎn)發(fā)給他。如果大家覺(jué)得我們的文章有幫助,歡迎點(diǎn)贊分享評(píng)論轉(zhuǎn)發(fā),一鍵三連。

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

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

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