大家都知道我們常用的 SpringBoot
項目最終在線上運行的時候都是通過啟動 java -jar xxx.jar
命令來運行的。
那你有沒有想過一個問題,那就是當我們執行 java -jar
命令后,到底底層做了什么就啟動了我們的 SpringBoot
應用呢?
或者說一個 SpringBoot
的應用到底是如何運行起來的呢?今天阿粉就帶大家來看下。
認識 jar
在介紹 java -jar
運行原理之前我們先看一下 jar
包里面都包含了哪些內容,我們準備一個 SpringBoot
項目,通過在 https://start.spring.io/ 上我們可以快速創建一個 SpringBoot
項目,下載一個對應版本和報名的 zip
包。
下載后的項目我們在 pom
依賴里面可以看到有如下依賴,這個插件是我們構建可執行 jar
的前提,所以如果想要打包成一個 jar
那必須在 pom
有增加這個插件,從 start.spring.io
上創建的項目默認是會帶上這個插件的。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
接下來我們執行 mvn package
,執行完過后在項目的 target
目錄里面我們可以看到有如下兩個 jar
包,我們分別把這兩個 jar
解壓一下看看里面的內容,.original
后綴的 jar 需要把后面的 .original
去掉就可以解壓了。jar
文件的解壓跟我們平常的 zip
解壓是一樣的,jar
文件采用的是 zip
壓縮格式存儲,所以任何可以解壓 zip
文件的軟件都可以解壓 jar
文件。
解壓過后,我們對比兩種解壓文件,可以發現,兩個文件夾中的內容還是有很大區別的,如下所示,左側是 demo-jar-0.0.1-SNAPSHOT.jar
右側是對應的 original jar
。
其中有一些相同的文件夾和文件,比如 META-INF
,application.properties
等,而且我們可以明顯的看到左側的壓縮包中有項目需要依賴的所有庫文件,存放于 lib
文件夾中。
所以我們可以大膽的猜測,左側的壓縮包就是 spring-boot-maven-plugin
這個插件幫我們把依賴的庫以及相應的文件調整了一下目錄結構而生成的,事實其實也是如此。
java -jar 原理
首先我們要知道的是這個 java -jar
不是什么新的東西,而是 java
本身就自帶的命令,而且java -jar
命令在執行的時候,命令本身對于這個 jar
是不是 SpringBoot
項目是不感知的,只要是符合 Java
標準規范的jar
都可以通過這個命令啟動。
而在 Java
官方文檔顯示,當 -jar
參數存在的時候,jar
文件資源里面必須包含用 Main-Class
指定的一個啟動類,而且同樣根據規范這個資源文件 MANIFEST.MF
必須放在 /META-INF/
目錄下。對比我們上面解壓后的文件,可以看到在左側的資源文件 MANIFEST.MF
文件中有如圖所示的一行。
[圖片上傳失敗...(image-3db0a9-1670339533552)]
可以看到這里的 Main-Class
屬性配置的是 org.springframework.boot.loader.JarLauncher
,而如果小伙伴更仔細一點的話,會發現我們項目的啟動類也在這個文件里面,是通過 Start-Class
字段來表示的,Start-Class
這個屬性不是 Java
官方的屬性。
由此我們先大膽的猜測一下,當我們在執行java -jar
的時候,由于我們的 jar
里面存在 MANIFEST.MF
文件,并且其中包含了 Main-Class
屬性且配置了 org.springframework.boot.loader.JarLauncher
類,通過調用 JarLauncher
類結合 Start-Class
屬性引導出我們項目的啟動類進行啟動。接下來我們就通過源碼來驗證一下這個猜想。
因為 JarLauncher
類是在 spring-boot-loader
模塊,所以我們在 pom
文件中增加如下依賴,就可以下載源碼進行跟蹤了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
通過源碼我們可以看到 JarLauncher
類的代碼如下
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);
}
}
其中有兩個點我們可以關注一下,第一個是這個類有一個 main
方法,這也是為什么 java -jar
命令可以進行引導的原因,畢竟 java
程序都是通過 main
方法進行運行的。其次是這里面有兩個路徑 BOOT-INF/classes/
和 BOOT-INF/lib/
這兩個路徑正好是我們的源碼路徑和第三方依賴路徑。
而 JarLauncher
類里面的 main()
方法主要是運行 Launcher
里面的 launch()
方法,這幾個類的關系圖如下所示
跟著代碼我們可以看到最終調用的是這個 run()
方法
而這里的參數 mainClass
和 launchClass
都是通過通過下面的邏輯獲取的,都是通過資源文件里面的 Start-Class
來進行獲取的,這里正是我們項目的啟動類,由此可以看到我們上面的猜想是正確的。
擴展
上面的類圖當中我們還可以看到除了有 JarLauncher
以外還有一個 WarLauncher
類,確實我們的 SpringBoot
項目也是可以配置成 war
進行部署的。我們只需要將打包插件里面的 jar
更換成 war
即可。大家可以自行嘗試重新打包解壓進行分析,這里 war
包部署方式只研究學習就好了,SpringBoot
應用還是盡量都使用 Jar
的方式進行部署。
總結
通過上面的內容我們知道了當我們在執行 java -jar
的時候,根據 java
官方規范會引導 jar
包里面 MANIFEST.MF
文件中的 Main-Class
屬性對應的啟動類,該啟動類中必須包含 main()
方法。
而對于我們 SpringBoot
項目構建的 ja
r 包,除了 Main-Class
屬性外還會有一個 Start-Class
屬性綁定的是我們項目的啟動類,當我們在執行 java -jar
的時候優先引導的是 org.springframework.boot.loader.JarLauncher#main
方法,該方法內部會通過引導 Start-Class
屬性來啟動我們的應用代碼。
通過上面的分析相比大家對于 SpringBoot
是如何通過 java -jar
進行啟動了有了一個詳細的了解,下次再有人問你 SpringBoot
項目是如何啟動的,請把這篇文章轉發給他。如果大家覺得我們的文章有幫助,歡迎點贊分享評論轉發,一鍵三連。