SpringBoot 系列-FatJar 啟動原理
之前有寫過一篇文章來介紹 JAR 檔案和 MENIFEST.MF 檔案,詳見:聊一聊 JAR 檔案和 MANIFEST.MF,在這篇文章中介紹了 JAR 檔案的內部結構。本篇將繼續延續前面的節奏,來介紹下,在 SpringBoot 中,是如何將一個 FatJar 執行起來的。
FatJar 解壓之後的檔案目錄
從 Spring 官網 或者通過 Idea 建立一個新的 SpringBoot 工程,方便起見,建議什麼依賴都不加,預設帶入的空的 SpringBoot 工程即可。
通過 maven 命令進行打包,打包成功之後得到的構建產物截圖如下:
在前面的文章中有提到,jar 包是zip 包的一種變種,因此也可以通過 unzip 來解壓
unzip -q guides-for-jarlaunch-0.0.1-SNAPSHOT.jar -d mock
複製程式碼
解壓的 mock 目錄,使用 tree 指令,看到整個解壓之後的 FatJar 的目錄結構如下(部分省略):
.
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties # 使用者-配置檔案
│ │ └── com
│ │ └── glmapper
│ │ └── bridge
│ │ └── boot
│ │ └── BootStrap.class # 使用者-啟動類
│ └── lib
│ ├── jakarta.annotation-api-1.3.5.jar
│ ├── jul-to-slf4j-1.7.28.jar
│ ├── log4j-xxx.jar # 表示 log4j 相關的依賴簡寫
│ ├── logback-xxx.jar # 表示 logback 相關的依賴簡寫
│ ├── slf4j-api-1.7.28.jar
│ ├── snakeyaml-1.25.jar
│ ├── spring-xxx.jar # 表示 spring 相關的依賴簡寫
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.glmapper.bridge.boot
│ └── guides-for-jarlaunch
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│ ├── # 省略
├── data
│ ├── # 省略
├── jar
│ ├── # 省略
└── util
└── SystemPropertyUtils.class
複製程式碼
簡單來看,FatJar 解壓之後包括三個資料夾:
├── BOOT-INF # 存放的是業務相關的,包括業務開發的類和配置檔案,以及依賴的jar
│ ├── classes
│ └── lib
├── META-INF # 包括 MANIFEST.MF 描述檔案和 maven 的構建資訊
│ ├── MANIFEST.MF
│ └── maven
└── org # SpringBoot 相關的類
└── springframework
複製程式碼
我們平時在 debug SpringBoot 工程的啟動流程時,一般都是從 SpringApplication#run 方法開始
@SpringBootApplication
public class BootStrap {
public static void main(String[] args) {
// 入口
SpringApplication.run(BootStrap.class,args);
}
}
複製程式碼
對於 java 程式來說,我們知道啟動入口必須有 main 函式,這裡看起來是符合條件的,但是有一點就是,通過 java 指令執行一個帶有 main 函式的類時,是不需要有 -jar 引數的,比如新建一個 BootStrap.java 檔案,內容為:
public class BootStrap {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
複製程式碼
通過 javac 編譯此檔案:
javac BootStrap.java
複製程式碼
然後就可以得到編譯之後的 .class 檔案 BootStrap.class ,此時可以通過 java 指令直接執行:
java BootStrap # 輸出 Hello World
複製程式碼
那麼對於 java -jar 呢?這個其實在 java 的官方檔案 中是有明確描述的:
- -jar filename
Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application's starting point.
When you use the -jar option,the specified JAR file is the source of all user classes,and other class path settings are ignored.
簡單說就是,java -jar 命令引導的具體啟動類必須配置在 MANIFEST.MF 資源的 Main-Class 屬性中。
那回過頭再去看下之前打包好、解壓之後的檔案目錄,找到 /META-INF/MANIFEST.MF 檔案,看下元資料:
Manifest-Version: 1.0
Implementation-Title: guides-for-jarlaunch
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.glmapper.bridge.boot.BootStrap
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.0.RELEASE
Created-By: Maven Archiver 3.4.0
# Main-Class 在這裡,指向的是 JarLauncher
Main-Class: org.springframework.boot.loader.JarLauncher
複製程式碼
org.springframework.boot.loader.JarLauncher 類存放在 org/springframework/boot/loader 下面:
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class # JarLauncher
├── # 省略
複製程式碼
這樣就基本理清楚了, FatJar 中,org.springframework.boot.loader 下面的類負責引導啟動 SpringBoot 工程,作為入口,BOOT-INF 中存放業務程式碼和依賴,META-INF 下存在元資料描述。
JarLaunch - FatJar 的啟動器
在分析 JarLaunch 之前,這裡插一下,org.springframework.boot.loader 下的這些類是如何被打包在 FatJar 裡面的
spring-boot-maven-plugin 打包 spring-boot-loader 過程
因為在新建的空的 SpringBoot 工程中並沒有任何地方顯示的引入或者編寫相關的類。實際上,對於每個新建的 SpringBoot 工程,可以在其 pom.xml 檔案中看到如下外掛:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
複製程式碼
這個是 SpringBoot 官方提供的用於打包 FatJar 的外掛,org.springframework.boot.loader 下的類其實就是通過這個外掛打進去的;
下面是此外掛將 loader 相關類打入 FatJar 的一個執行流程:
org.springframework.boot.maven#execute-> org.springframework.boot.maven#repackage -> org.springframework.boot.loader.tools.Repackager#repackage-> org.springframework.boot.loader.tools.Repackager#writeLoaderClasses-> org.springframework.boot.loader.tools.JarWriter#writeLoaderClasses
最終的執行方法就是下面這個方法,通過註釋可以看出,該方法的作用就是將 spring-boot-loader 的classes 寫入到 FatJar 中。
/**
* Write the required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses() throws IOException {
writeLoaderClasses(NESTED_LOADER_JAR);
}
複製程式碼
JarLaunch 基本原理
基於前面的分析,這裡考慮一個問題,能否直接通過 java BootStrap 來直接執行 SpringBoot 工程呢?這樣在不需要 -jar 引數和 JarLaunch 引導的情況下,直接使用最原始的 java 指令理論上是不是也可以,因為有 main 方法。
通過 java BootStrap
方式啟動
BootStrap 類的如下:
@SpringBootApplication
public class BootStrap {
public static void main(String[] args) {
SpringApplication.run(BootStrap.class,args);
}
}
複製程式碼
編譯之後,執行 java com.glmapper.bridge.boot.BootStrap
,然後丟擲異常了:
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
at com.glmapper.bridge.boot.BootStrap.main(BootStrap.java:13)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
複製程式碼
從異常堆疊來看,是因為找不到 SpringApplication 這個類;這裡其實還是比較好理解的,BootStrap 類中引入了 SpringApplication,但是這個類是在 BOOT-INF/lib 下的,而 java 指令在啟動時也沒有指定 class path 。
這裡不再贅述,通過 -classpath + -Xbootclasspath 的方式嘗試了下,貌似也不行,如果有通過 java 指令直接執行成功的,歡迎留言溝通。
通過 java JarLaunch 啟動
再通過 java org.springframework.boot.loader.JarLauncher
方式啟動,可以看到是可以的。
那這裡基本可以猜到,JarLauncher 方式啟動時,一定會通過某種方式將所需要依賴的 JAR 檔案作為 BootStrap 的依賴引入進來。下面就來簡單分析下 JarLauncher 啟動時,作為啟動引導類,它做了哪些事情。
基本原理分析
JarLaunch 類的定義如下:
public class JarLauncher extends ExecutableArchiveLauncher {
// BOOT-INF/classes/
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
// BOOT-INF/lib/
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
// 空建構函式
public JarLauncher() {
}
// 帶有指定 Archive 的建構函式
protected JarLauncher(Archive archive) {
super(archive);
}
// 是否是可巢狀的物件
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
// main 函式
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
複製程式碼
通過程式碼,我們很明顯可以看到幾個關鍵的資訊點:
-
BOOT_INF_CLASSES
和BOOT_INF_LIB
兩個常量對應的是前面解壓之後的兩個檔案目錄 - JarLaunch 中包含一個 main 函式,作為啟動入口
但是單從 main 來看,只是構造了一個 JarLaunch 物件,然後執行其 launch 方法,並沒有我們期望看到的構建所需依賴的地方。實際上這部分是在 JarLaunch 的父類 ExecutableArchiveLauncher 的建構函式中來完成的。
public ExecutableArchiveLauncher() {
try {
// 構建 archive
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
// 構建 Archive
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
// 這裡就是拿到當前的 classpath
// /Users/xxx/Documents/test/glmapper-springboot-study-guides/guides-for-jarlaunch/target/mock/
String path = (location != null) ? location.getSchemeSpecificPart() : null;
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException("Unable to determine code source archive from " + root);
}
// 構建 Archive
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
複製程式碼
PS: 關於 Archive 的概念這裡由於篇幅有限,不再展開說明。
通過上面構建了一個 Archive ,然後繼續執行 launch 方法:
protected void launch(String[] args) throws Exception {
// 註冊協議,利用了 java.net.URLStreamHandler 的擴充套件機制,SpringBoot
// 擴展出了一種可以解析 jar in jar 的協議
JarFile.registerUrlProtocolHandler();
// 通過 classpath 來構建一個 ClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// launch
launch(args,getMainClass(),classLoader);
}
複製程式碼
下面值需要關注下 getMainClass() 方法即可,這裡就是獲取 MENIFEST.MF 中指定的 Start-Class ,實際上就是我們的工程裡面的 BootStrap 類:
@Override
protected String getMainClass() throws Exception {
// 從 archive 中拿到 Manifest
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 獲取 Start-Class
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
// 返回 mainClass
return mainClass;
}
複製程式碼
最終是通過構建了一個 MainMethodRunner 例項物件,然後通過反射的方式呼叫了 BootStrap 類中的 main 方法:
小結
本文主要從 JarLaunch 的角度分析了下 SpringBoot 的啟動方式,對常規 java 方式和 java -jar 等啟動方式進行了簡單的演示;同時簡單闡述了下 JarLaunch 啟動的基本工作原理。對於其中 構建 Archive 、自定義協議 Handler 等未做深入探究,後面也會針對相關點再做單獨分析。