一.可執(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信息如:

填寫完信息后,點(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.JarFileEntry和ExplodedArchive.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)行;