1. 概述
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin
,可以方便的將 Spring Boot 項目打成 jar
包或者 war
包。
考慮到部署的便利性,我們絕大多數 99.99% 的場景下,我們會選擇打成 jar
包。這樣,我們就無需在部署項目的服務器上,配置相應的 Tomcat、Jetty 等 Servlet 容器。
那麼,jar
包是如何運行,並啓動 Spring Boot 項目的呢?這個就是本文的目的,一起弄懂 Spring Boot jar
包的運行原理。
下面,我們來打開一個 Spring Boot jar
包,看看其裏面的結構。如下圖所示,一共分成四部分:
Spring Boot jar
包
-
①
META-INF
目錄:通過MANIFEST.MF
文件提供jar
包的元數據,聲明瞭jar
的啓動類。 -
②
org
目錄:爲 Spring Boot 提供的spring-boot-loader
項目,它是java -jar
啓動 Spring Boot 項目的祕密所在,也是稍後我們將深入瞭解的部分。“
Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using
java -jar
. Generally you will not need to usespring-boot-loader
directly, but instead work with the Gradle or Maven plugin. -
③
BOOT-INF/lib
目錄:我們 Spring Boot 項目中引入的依賴的jar
包們。spring-boot-loader
項目很大的一個作用,就是解決jar
包裏嵌套jar
的情況,如何加載到其中的類。 -
④
BOOT-INF/classes
目錄:我們在 Spring Boot 項目中 Java 類所編譯的.class
、配置文件等等。
先簡單劇透下,spring-boot-loader
項目需要解決兩個問題:
-
第一,如何引導執行我們創建的 Spring Boot 應用的啓動類,例如上述圖中的 Application 類。
-
第二,如何加載
BOOT-INF/class
目錄下的類,以及BOOT-INF/lib
目錄下內嵌的jar
包中的類。
下面,尾隨艿艿,一起來抽絲剝繭!
2. MANIFEST.MF
我們來查看 META-INF/MANIFEST.MF
文件,裏面的內容如下:
Manifest-Version: 1.0
Implementation-Title: lab-39-demo
Implementation-Version: 2.2.2.RELEASE
Start-Class: cn.iocoder.springboot.lab39.skywalkingdemo.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.2.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
它實際是一個 Properties 配置文件,每一行都是一個配置項目。重點來看看兩個配置項:
-
Main-Class
配置項:Java 規定的jar
包的啓動類,這裏設置爲spring-boot-loader
項目的 JarLauncher 類,進行 Spring Boot 應用的啓動。 -
Start-Class
配置項:Spring Boot 規定的主啓動類,這裏設置爲我們定義的 Application 類。
“小知識補充:爲什麼會有
Main-Class
/Start-Class
配置項呢?因爲我們是通過 Spring Boot 提供的 Maven 插件spring-boot-maven-plugin
進行打包,該插件將該配置項寫入到MANIFEST.MF
中,從而能讓spring-boot-loader
能夠引導啓動 Spring Boot 應用。
可能胖友會有疑惑,Start-Class
對應的 Application 類自帶了 #main(String[] args)
方法,爲什麼我們不能直接運行會如何呢?我們來簡單嘗試一下哈,控制檯執行如下:
$ java -classpath lab-39-demo-2.2.2.RELEASE.jar cn.iocoder.springboot.lab39.skywalkingdemo.Application
錯誤: 找不到或無法加載主類 cn.iocoder.springboot.lab39.skywalkingdemo.Application
直接找不到 Application 類,因爲它在 BOOT-INF/classes
目錄下,不符合 Java 默認的 jar
包的加載規則。因此,需要通過 JarLauncher 啓動加載。
當然實際還有一個更重要的原因,Java 規定可執行器的 jar
包禁止嵌套其它 jar
包。但是我們可以看到 BOOT-INF/lib
目錄下,實際有 Spring Boot 應用依賴的所有 jar
包。因此,spring-boot-loader
項目自定義實現了 ClassLoader 實現類 LaunchedURLClassLoader,支持加載 BOOT-INF/classes
目錄下的 .class
文件,以及 BOOT-INF/lib
目錄下的 jar
包。
3. JarLauncher
JarLauncher 類是針對 Spring Boot jar
包的啓動類,整體類圖如下所示:
JarLauncher 類圖
“友情提示:WarLauncher 類,是針對 Spring Boot
war
包的啓動類,後續胖友可以自己瞅瞅,差別並不大哈~
JarLauncher 的源碼比較簡單,如下圖所示:
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
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);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
通過 #main(String[] args)
方法,創建 JarLauncher 對象,並調用其 #launch(String[] args)
方法進行啓動。整體的啓動邏輯,其實是由父類 Launcher 所提供,如下圖所示:
Launcher 啓動過程
父類 Launcher 的 #launch(String[] args)
方法,代碼如下:
// Launcher.java
protected void launch(String[] args) throws Exception {
// <1> 註冊 URL 協議的處理器
JarFile.registerUrlProtocolHandler();
// <2> 創建類加載器
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// <3> 執行啓動類的 main 方法
launch(args, getMainClass(), classLoader);
}
-
<1>
處,調用 JarFile 的#registerUrlProtocolHandler()
方法,註冊 Spring Boot 自定義的 URLStreamHandler 實現類,用於jar
包的加載讀取。 -
<2>
處,調用自身的#createClassLoader(List<Archive> archives)
方法,創建自定義的 ClassLoader 實現類,用於從jar
包中加載類。 -
<3>
處,執行我們聲明的 Spring Boot 啓動類,進行 Spring Boot 應用的啓動。
簡單來說,就是整一個可以讀取 jar
包中類的加載器,保證 BOOT-INF/lib
目錄下的類和 BOOT-classes
內嵌的 jar
中的類能夠被正常加載到,之後執行 Spring Boot 應用的啓動。
下面,我們逐行代碼來看看噢。即將代碼多多,保持淡定,嘿嘿~
3.1 registerUrlProtocolHandler
“友情提示:對應
JarFile.registerUrlProtocolHandler();
代碼段,不要迷路。
JarFile 是 java.util.jar.JarFile
的子類,如下所示:
public class JarFile extends java.util.jar.JarFile {
// ... 省略其它代碼
}
JarFile 主要增強支持對內嵌的 jar
包的獲取。如下圖所示:
讀取內嵌的 jar
包的演示
OK,介紹完之後,讓我們回到 JarFile 的 #registerUrlProtocolHandler()
方法,註冊 Spring Boot 自定義的 URL 協議的處理器。代碼如下:
// JarFile.java
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/
public static void registerUrlProtocolHandler() {
// 獲得 URLStreamHandler 的路徑
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
// 將 Spring Boot 自定義的 HANDLERS_PACKAGE(org.springframework.boot.loader) 補充上去
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
// 重置已緩存的 URLStreamHandler 處理器們
resetCachedUrlHandlers();
}
/**
* Reset any cached handlers just in case a jar protocol has already been used.
* We reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
* should have no effect other than clearing the handlers cache.
*
* 重置 URL 中的 URLStreamHandler 的緩存,防止 `jar://` 協議對應的 URLStreamHandler 已經創建
* 我們通過設置 URLStreamHandlerFactory 爲 null 的方式,清空 URL 中的該緩存。
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
} catch (Error ex) {
// Ignore
}
}
-
胖友先跟着註釋,自己閱讀下如上的代碼~
目的很明確,通過將 org.springframework.boot.loader
包設置到 "java.protocol.handler.pkgs"
環境變量,從而使用到自定義的 URLStreamHandler 實現類 Handler,處理 jar:
協議的 URL。
“友情提示:這裏我們暫時不深入 Handler 的源碼,避免直接走的太深,丟失了主幹。後續胖友可結合《Java URL 協議擴展實現》文章,進行 Handler 的實現理解。
另外,HandlerTests 提供的單元測試,也是非常有幫助的~
3.2 createClassLoader
“友情提示:對應
ClassLoader classLoader = createClassLoader(getClassPathArchives())
代碼段,不要迷路。
3.2.1 getClassPathArchives
首先,我們先來看看 #getClassPathArchives()
方法,它是由 ExecutableArchiveLauncher 所實現,代碼如下:
// ExecutableArchiveLauncher.java
private final Archive archive;
@Override
protected List<Archive> getClassPathArchives() throws Exception {
// <1> 獲得所有 Archive
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
// <2> 後續處理
postProcessClassPathArchives(archives);
return archives;
}
protected abstract boolean isNestedArchive(Archive.Entry entry);
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
“友情提示:這裏我們會看到一個 Archive 對象,先可以暫時理解成一個一個的檔案,稍後會清晰認識的~
<1>
處,this::isNestedArchive
代碼段,創建了 EntryFilter 匿名實現類,用於過濾 jar
包不需要的目錄。
// Archive.java
/**
* Represents a single entry in the archive.
*/
interface Entry {
/**
* Returns {@code true} if the entry represents a directory.
* @return if the entry is a directory
*/
boolean isDirectory();
/**
* Returns the name of the entry.
* @return the name of the entry
*/
String getName();
}
/**
* Strategy interface to filter {@link Entry Entries}.
*/
interface EntryFilter {
/**
* Apply the jar entry filter.
* @param entry the entry to filter
* @return {@code true} if the filter matches
*/
boolean matches(Entry entry);
}
這裏在它的內部,調用了 #isNestedArchive(Archive.Entry entry)
方法,它是由 JarLauncher 所實現,代碼如下:
// JarLauncher.java
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 如果是目錄的情況,只要 BOOT-INF/classes/ 目錄
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果是文件的情況,只要 BOOT-INF/lib/ 目錄下的 `jar` 包
return entry.getName().startsWith(BOOT_INF_LIB);
}
-
目的就是過濾獲得,
BOOT-INF/classes/
目錄下的類,以及BOOT-INF/lib/
的內嵌jar
包。
<1>
處,this.archive.getNestedArchives
代碼段,調用 Archive 的 #getNestedArchives(EntryFilter filter)
方法,獲得 archive
內嵌的 Archive 集合。代碼如下:
// Archive.java
/**
* Returns nested {@link Archive}s for entries that match the specified filter.
* @param filter the filter used to limit entries
* @return nested archives
* @throws IOException if nested archives cannot be read
*/
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
Archive 接口,是 spring-boot-loader
項目定義的檔案抽象,其子類如下圖所示:
Archive 類圖
-
ExplodedArchive 是針對目錄的 Archive 實現類。
-
JarFileArchive 是針對
jar
包的 Archive 實現類。
“友情提示:這塊可能有一丟丟複雜,胖友喫耐心哈~
那麼,我們在 ExecutableArchiveLauncher 的 archive
屬性是怎麼來的呢?答案在 ExecutableArchiveLauncher 的構造方法中,代碼如下:
// ExecutableArchiveLauncher.java
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
// ... 省略其它
}
// Launcher.java
public abstract class Launcher {
protected final Archive createArchive() throws Exception {
// 獲得 jar 所在的絕對路徑
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
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);
}
// 如果是目錄,則使用 ExplodedArchive 進行展開
// 如果不是目錄,則使用 JarFileArchive
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
}
根據根路徑是否爲目錄的情況,創建 ExplodedArchive 或 JarFileArchive 對象。那麼問題就來了,這裏的 root
是什麼呢?艿艿一波騷操作,終於輸出了答案,如下圖所示:
root
是什麼!
root
路徑爲 jar
包的絕對地址,也就是說創建 JarFileArchive 對象。原因是,Launcher 所在包爲 org
下,它的根目錄當然是 jar
包的絕對路徑哈!
😈 現在是不是對 Archive 稍微有點感覺落?繼續附加如下代碼,打印 JarFileArchive 的 #getNestedArchives(EntryFilter filter)
方法的執行結果。
// ========== 附加代碼:
// 創建 Archive 對象
Archive archive = new JarFileArchive(root);
// 創建 EntryFilter 對象
Archive.EntryFilter filter = new Archive.EntryFilter() {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
@Override
public boolean matches(Archive.Entry entry) {
// 如果是目錄的情況,只要 BOOT-INF/classes/ 目錄
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果是文件的情況,只要 BOOT-INF/lib/ 目錄下的 `jar` 包
return entry.getName().startsWith(BOOT_INF_LIB);
}
};
// 執行讀取
for (Archive item : archive.getNestedArchives(filter)) {
System.out.println(item.getUrl());
}
// ========== 執行結果:
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/classes!/
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.2.RELEASE.jar!/
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-starter-2.2.2.RELEASE.jar!/
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-2.2.2.RELEASE.jar!/
... 省略其他 jar 包
從執行結果可以看出,BOOT-INF/classes/
目錄被歸類爲一個 Archive 對象,而 BOOT-INF/lib/
目錄下的每個內嵌 jar
包都對應一個 Archive 對象。
來來來,回過頭來看看 JarFileArchive 的 #getNestedArchives(EntryFilter filter)
方法的源碼,如下圖所示:
JarFileArchive 的 #getNestedArchives(EntryFilter filter)
方法
現在是不是明白了噢!良心如我,哈哈哈!
“友情提示:上述的測試代碼,可以訪問 lab-39-demo 項目查看。
3.2.2 createClassLoader
然後,我再來看看 #createClassLoader(List<Archive> archives)
方法,它是由 ExecutableArchiveLauncher 所實現,代碼如下:
// ExecutableArchiveLauncher.java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
// 獲得所有 Archive 的 URL 地址
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
// 創建加載這些 URL 的 ClassLoader
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
基於獲得的 Archive 數組,創建自定義 ClassLoader 實現類 LaunchedURLClassLoader,通過它來加載 BOOT-INF/classes
目錄下的類,以及 BOOT-INF/lib
目錄下的 jar
包中的類。
進一步的解析,我們在「5. LaunchedURLClassLoader」小節中,進行分享哈!
3.3 launch
“友情提示:對應
launch(args, getMainClass(), classLoader)
代碼段,不要迷路。
3.3.1 getMainClass
首先,我們先來看看#getMainClass()
方法,它是由 ExecutableArchiveLauncher 所實現,代碼如下:
// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
// 獲得啓動的類的全名
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
從 jar
包的 MANIFEST.MF
文件的 Start-Class
配置項,,獲得我們設置的 Spring Boot 的主啓動類。
3.3.2 createMainMethodRunner
然後,我們再來看看 #launch()
方法,它是由 Launcher 所實現,代碼如下:
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
// <1> 設置 LaunchedURLClassLoader 作爲類加載器
Thread.currentThread().setContextClassLoader(classLoader);
// <2> 創建 MainMethodRunner 對象,並執行 run 方法,啓動 Spring Boot 應用
createMainMethodRunner(mainClass, args, classLoader).run();
}
該方法負責最終的 Spring Boot 應用真正的啓動。
-
<1>
處:設置「3.2.2 createClassLoader」創建的 LaunchedURLClassLoader 作爲類加載器,從而保證能夠從jar
加載到相應的類。 -
<2>
處,調用#createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader)
方法,創建 MainMethodRunner 對象,並執行其#run()
方法來啓動 Spring Boot 應用。
下面,我們來看看 MainMethodRunner 類,負責 Spring Boot 應用的啓動。代碼如下:
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
// <1> 加載 Spring Boot
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
// <2> 反射調用 main 方法
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}
-
<1>
處:通過 LaunchedURLClassLoader 類加載器,加載到我們設置的 Spring Boot 的主啓動類。 -
<2>
處:通過反射調用主啓動類的#main(String[] args)
方法,啓動 Spring Boot 應用。這裏也告訴了我們答案,爲什麼我們通過編寫一個帶有#main(String[] args)
方法的類,就能夠啓動 Spring Boot 應用。
4. LaunchedURLClassLoader
LaunchedURLClassLoader 是 spring-boot-loader
項目自定義的類加載器,實現對 jar
包中 META-INF/classes
目錄下的類和 META-INF/lib
內嵌的 jar
包中的類的加載。
“FROM 《維基百科 —— Java 類加載器》
Java 類加載器是 Java 運行時環境的一個部件,負責動態加載 Java 類到 Java 虛擬機的內存空間中。類通常是按需加載,即第一次使用該類時才加載。
由於有了類加載器,Java 運行時系統不需要知道文件與文件系統。對學習類加載器而言,掌握 Java 的委派概念是很重要的。每個 Java 類必須由某個類加載器裝入到內存。
在「3.2.2 createClassLoader」小節中,我們可以看到 LaunchedURLClassLoader 的創建代碼如下:
// ExecutableArchiveLauncher.java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
// 獲得所有 Archive 的 URL 地址
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
// 創建加載這些 URL 的 ClassLoader
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
// LaunchedURLClassLoader.java
public class LaunchedURLClassLoader extends URLClassLoader {
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
}
-
第一個參數
urls
,使用的是 Archive 集合對應的 URL 地址們,從而告訴 LaunchedURLClassLoader 讀取jar
的地址。 -
第二個參數
parent
,設置 LaunchedURLClassLoader 的父加載器。這裏後續胖友可以理解下,類加載器的雙親委派模型,這裏就拓展開了。
LaunchedURLClassLoader 的實現代碼並不多,我們主要來看看它是如何從 jar
包中加載類的。核心如下圖所示:
-
<1>
處,在通過父類的#getPackage(String name)
方法獲取不到指定類所在的包時,會通過遍歷urls
數組,從jar
包中加載類所在的包。當找到包時,會調用#definePackage(String name, Manifest man, URL url)
方法,設置包所在的 Archive 對應的url
。 -
<2>
處,調用父類的#loadClass(String name, boolean resolve)
方法,加載對應的類。
如此,我們就實現了通過 LaunchedURLClassLoader 加載 jar
包中內嵌的類。
666. 彩蛋
總體來說,Spring Boot jar
啓動的原理是非常清晰的,整體如下圖所示:
Spring Boot jar
啓動原理
紅色部分,解決 jar
包中的類加載問題:
-
通過 Archive,實現
jar
包的遍歷,將META-INF/classes
目錄和META-INF/lib
的每一個內嵌的jar
解析成一個 Archive 對象。 -
通過 Handler,處理
jar:
協議的 URL 的資源讀取,也就是讀取了每個 Archive 裏的內容。 -
通過 LaunchedURLClassLoader,實現
META-INF/classes
目錄下的類和META-INF/classes
目錄下內嵌的jar
包中的類的加載。具體的 URL 來源,是通過 Archive 提供;具體 URL 的讀取,是通過 Handler 提供。
橘色部分,解決 Spring Boot 應用的啓動問題:
-
通過 MainMethodRunner ,實現 Spring Boot 應用的啓動類的執行。
當然,上述的一切都是通過 Launcher 來完成引導和啓動,通過 MANIFEST.MF
進行具體配置。
😈 生活如此美好,本文就此結束!
另外,本文有兩個部分,胖友可以自己再去擼一擼,玩一玩:
-
WarLauncher 類:實現 Spring Boot
war
包的啓動。 -
org.springframework.boot.loader.jar
包:具體讀取jar
的邏輯。