前言
SpringBoot項(xiàng)目通常都是由主類的main函數(shù)開始啟動(dòng)妨托,好奇心驅(qū)使我想搞明白通常項(xiàng)目所有的內(nèi)容都被打成了一個(gè)fat jar,按理說jar包中再包含的jar是沒有辦法被jdk加載的结缚,所以這個(gè)過程SpringBoot又是如何讓單個(gè)jar直接運(yùn)行起來的?
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
spring-boot-loader
通過解壓SpringBoot的maven插件二次打包的jar凡傅,可以看到目錄如下:
-
BOOT-INF/classes
下是spring-boot項(xiàng)目中編寫的java源碼編譯后的class -
BOOT-INF/lib
下是spring-boot項(xiàng)目依賴的所有jar包 -
META-INF
是jar的信息粪糙,包含主類和sring-boot添加的額外的信息記錄
-org.springgramework
下則是maven插件裝載進(jìn)去的class文件,也就是fat jar可以運(yùn)行起來的源碼
app
├── BOOT-INF
│ ├── classes
│ └── lib
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
└── org
└── springframework
當(dāng)然鸯两,直接看org.springframework
下反編譯的源碼有點(diǎn)晦澀闷旧,畢竟是反編譯而來的。查看spring boot的源碼可以在spring-boot-tools項(xiàng)目下看到spring-boot-loader子項(xiàng)目钧唐,這個(gè)其實(shí)就是maven插件裝載到fat jar中的class文件的源碼忙灼,所以閱讀這個(gè)子項(xiàng)目的源碼基本就可以搞清楚,spring boot的fat jar是如何把自己跑起來的。
SpringBoot項(xiàng)目的啟動(dòng)方式
1. idea中的啟動(dòng)
通常在IDEA中默認(rèn)的啟動(dòng)方式是直接通過主類啟動(dòng)该园,所有依賴的jar都通過jdk的參數(shù)添加進(jìn)來酸舍。很明顯,這種啟動(dòng)方式?jīng)]有借助于spring-boot-loader里初,是正常的java程序運(yùn)行方式啟動(dòng)父腕。
這種啟動(dòng)方式經(jīng)常用于開發(fā),畢竟直接啟動(dòng)更快一些青瀑。但是也有弊端,那就是通過command line的形式啟動(dòng)時(shí)萧诫,如果依賴的jar過多斥难,會(huì)導(dǎo)致拼接的命令過長而報(bào)錯(cuò),所以此中方式通常沒有辦法用于中大型項(xiàng)目
除了在idea中借助于開發(fā)工具拼接運(yùn)行命令之外帘饶,spring boot支持三種常見的啟動(dòng)方式哑诊。
- jar
- war
- properties
2. jar
jar方式就是借助于spring boot的maven插件二次打包后的fat jar的形式啟動(dòng)。對應(yīng)spring-boot-loader項(xiàng)目中的JarLauncher類及刻,源碼如下(源碼中的注釋镀裤,部分翻譯,部分為我自己添加缴饭,幫助閱讀):
/**
* 用于基于JAR形式的啟動(dòng)暑劝,該jar依賴的所有的其他jar包在/BOOT-INF/lib路徑下
* 該jar對應(yīng)的spring boot的項(xiàng)目的java類全部位于/BOOT-INF/classes下
*/
public class JarLauncher extends ExecutableArchiveLauncher {
// 依賴的class文件的路徑
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
// 依賴的其他jar文件的路徑
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
// 判斷entry是否為內(nèi)嵌依賴jar的,判斷的依據(jù)是名稱
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
// jar形式的main-class
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
由于jar形式的啟動(dòng)是最常見的方式颗搂,所以本文會(huì)著重jar形式啟動(dòng)的分析担猛。
3. war包形式
在spring boot之前,大多數(shù)的spring mvc項(xiàng)目都是打成war包丢氢,置于tomcat的webapp目錄下來運(yùn)行傅联,所以springboot也是支持這種形式的啟動(dòng),只要在spring boot的maven插件中將打包的目前格式改為WAR即可疚察。在loader項(xiàng)目中對應(yīng)的啟動(dòng)類為:WarLauncher
/**
* 注釋翻譯:用于war包形式的啟動(dòng)蒸走,只支持標(biāo)準(zhǔn)的WAR歸檔文件。
* 三方依賴的jar位于 WEB-INF/lib貌嫡, 也可以為WEB-INF/lib-provided比驻,
* 項(xiàng)目的class文件位于WEB-INF/classes路徑下。
*/
public class WarLauncher extends ExecutableArchiveLauncher {
private static final String WEB_INF = "WEB-INF/";
private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
private static final String WEB_INF_LIB = WEB_INF + "lib/";
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
// 部分代碼刪除衅枫。嫁艇。。
// 啟動(dòng)主類
public static void main(String[] args) throws Exception {
new WarLauncher().launch(args);
}
}
4. 基于配置屬性的形式啟動(dòng)
基于自定義配置的啟動(dòng)方式弦撩,兼容Fat JAR步咪。這種啟動(dòng)方式就比較靈活,可以通過三方插件將項(xiàng)目打成多種格式益楼,或者不二次打包等等猾漫,最后通過配置解析來啟動(dòng)spring boot項(xiàng)目点晴。
比如,可以將依賴悯周,配置文件粒督,啟動(dòng)類打包到指定目錄,然后按照如下方式啟動(dòng):
java -Dloader.main=xxx.xxx.Application \ # 主類
-Dloader.path=lib,config,resource,xxx.jar \ 依賴和配置資源
-Dspring.profiles.active=dev \ // profiles
org.springframework.boot.loader.PropertiesLauncher // 啟動(dòng)類
所以Spring Boot的loader項(xiàng)目禽翼,就是提供spring boot應(yīng)用可以在不同場景和需求下都可以正常啟動(dòng)的能力屠橄,完成了從打包和實(shí)際項(xiàng)目運(yùn)行的橋接過程。
接下來闰挡,我們以JAR啟動(dòng)的方式锐墙,來分析分析,Spring boot到底是如何完成啟動(dòng)過程的:
可執(zhí)行Jar啟動(dòng)
jar形式的啟動(dòng)长酗,主類為JarLauncher
溪北,其繼承自ExecutableArchiveLauncher
,最上層的父類為Launcher
夺脾,同時(shí)也是所有其他啟動(dòng)形式的頂層父類之拨。
JarLauncher中代碼不多,直接調(diào)用了Launcher中的launch方法咧叭,所以我們的代碼分析也從這里開始蚀乔。
launch方法了主要干了三件事情,
- 第一是注冊擴(kuò)展protocol handler
- 第二是獲取fat jar中所有的歸檔(三方j(luò)ar菲茬,class乙墙,資源文件等等)來創(chuàng)建自定義的類加載器(ClassLoader)
- 最后使用創(chuàng)建好的類加載器,攜帶啟動(dòng)參數(shù)生均,創(chuàng)建主類啟動(dòng)對象听想,啟動(dòng)主類(主類在loader中為Start Class,其實(shí)就是spring boot應(yīng)用的啟動(dòng)類马胧,在spring的maven插件中被打包定義為Start class)
protected void launch(String[] args) throws Exception {
// 注冊URL protocol handler
JarFile.registerUrlProtocolHandler();
// 獲取fat jar中的archives(也就是三方j(luò)ar汉买,class,以及其他資源文件),創(chuàng)建類加載器
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 獲取sub class(也就是spring boot 應(yīng)用的主類)佩脊,使用啟動(dòng)參數(shù)和創(chuàng)建好的class loader啟動(dòng)
launch(args, getMainClass(), classLoader);
}
接下來蛙粘,我們針對這三個(gè)步驟展開來講。
1注冊擴(kuò)展UrlProtocolHandler
其實(shí)這個(gè)方法相當(dāng)于在啟動(dòng)java應(yīng)用時(shí)添加參數(shù):-Djava.protocol.handler.pkgs=xxx.xxx.xxx
威彰,其作用就是對Url類支持的協(xié)議進(jìn)行擴(kuò)展出牧。多個(gè)指定的包的地址使用|
來連接。
/**
* 翻譯:注冊一個(gè)handler歇盼,以便定位URLStreamHandler來處理jar urls
* 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() {
// 獲取當(dāng)前jvm中的handler參數(shù)
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
// 如果已有參數(shù)為空舔痕,則直接指定springboot的handler,否則|拼接進(jìn)行擴(kuò)展
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
// 最后重置緩存的handler
resetCachedUrlHandlers();
}
那Spring boot擴(kuò)展這個(gè)來干嘛呢,方法注釋上說時(shí)為了定位URLStreamHandler
來處理jar文件伯复,后續(xù)我們分析的過程中再繼續(xù)看慨代。
- 獲取ClassPath下的文件資源
createClassLoader(getClassPathArchives());
雖然第二個(gè)步驟只有一句話,但這其實(shí)就是SpringBoot可以直接啟動(dòng)jar文件的核心邏輯啸如,所以展開來講侍匙,首先是獲取jar中的資源文件。
getClassPathArchives
在Launcher中是abstract的叮雳,其具體實(shí)現(xiàn)在ExecutableArchiveLauncher
中想暗。getClassPathArchives
的實(shí)現(xiàn)其實(shí)代碼不多,核心方法是getNestedArchives
(獲取嵌套的jar等文件)帘不〗酰看到這里其實(shí)我們就能稍微理解為什么Spring Boot能夠直接啟動(dòng)并直接嵌套自身jar中的其他jar了,其邏輯就是通過某種方式解析并獲取jar(猜測是作為普通資源文件獲取厌均,然后讀內(nèi)存或者寫到其他目錄,再加載進(jìn)來告唆,不過因?yàn)槲乙呀?jīng)讀過了棺弊,所以猜測其實(shí)是對的,哈哈哈)然后傳遞給自定義的classLoader
加載擒悬,從而完成了依賴的jar的加載模她。
/**
* 獲取class path下的文件,jar啟動(dòng)方式其實(shí)主要是獲取嵌套在fat jar中的其他三方j(luò)ar
*/
@Override
protected List<Archive> getClassPathArchives() throws Exception {
// 獲取嵌套的文檔文件懂牧,
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
// 后置處理歸檔文件
postProcessClassPathArchives(archives);
// 返回結(jié)果
return archives;
}
看著這里其實(shí)有點(diǎn)疑惑侈净,this.archive
是啥,之前沒有提到過僧凤。JarLauncher
剛剛是在main方法中無參new的畜侦,所以就是隱含的執(zhí)行ExecutableArchiveLauncher
的無參的構(gòu)造方法,這個(gè)archive
就是在那個(gè)時(shí)候?qū)嵗摹?/p>
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
ok躯保,所以這個(gè)時(shí)候需要擱置剛才的邏輯旋膳,先看看這個(gè)archive
是什么東西,才能接著看它是如何獲取jar中的archives的途事。
createArchive
方法主要有兩個(gè)邏輯验懊,首先是獲取當(dāng)前類對應(yīng)的絕對路徑。接著判斷尸变,如果絕對路徑對應(yīng)的是目錄义图,則archive
就是ExplodedArchive
,當(dāng)前我們假設(shè)是用jar啟動(dòng)的召烂,那絕對路徑對應(yīng)的就是jar文件本省碱工,此時(shí)this.archive
就會(huì)被實(shí)例化成JarFileArchive
。
看到這里就清楚了,this.archive
是JarFileArchive
的實(shí)例痛垛。所以草慧,獲取jar中的archive邏輯就是在這個(gè)類中實(shí)現(xiàn)的。
ExplodedArchive
的實(shí)現(xiàn)會(huì)用于war
和properties
的啟動(dòng)形式的archives的獲取匙头。
/**
* 創(chuàng)建Archive
*/
protected final Archive createArchive() throws Exception {
// 反射獲取當(dāng)前jar的 protectionDomain,可以理解為一個(gè)jar會(huì)對應(yīng)一個(gè)ProtectionDomain漫谷,主要是jar中資源的權(quán)限檢查和控制
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
// 然后獲取當(dāng)前類的codeSource
CodeSource codeSource = protectionDomain.getCodeSource();
// 最后獲取當(dāng)前類的路徑URL,再拿到path
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");
}
// 有了path蹂析,就可以將其包裝為java的抽象文件類
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException("Unable to determine code source archive from " + root);
}
// 最后如果是目錄舔示,archive就會(huì)被實(shí)例化成ExplodedArchive, 如果是Jar形式啟動(dòng),那就是非目錄电抚,所以實(shí)例化成JarFileArchive
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
到這里惕稻,就到最重要的邏輯:解析fat jar中的資源文件,包括三方j(luò)ar蝙叛,class文件俺祠,資源文件等等。
首先看入口方法借帘,方法的邏輯很簡單蜘渣。首先是迭代自身,獲取entry肺然,第二是包裝entry為Archive
蔫缸,最后返回。所以對應(yīng)的搞清楚這兩個(gè)邏輯际起,就能理解jar中的資源文件是如何解析的拾碌。
/**
* 獲取嵌套的archives
* @param filter the filter used to limit entries
*/
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
// 迭代自己本身,通過外部傳遞的isNestedArchive街望,JarLauncher中實(shí)現(xiàn)了校翔,通過class的前綴判斷
for (Entry entry : this) {
if (filter.matches(entry)) {
// 通過entry,獲取并包裝為Archive
nestedArchives.add(getNestedArchive(entry));
}
}
// 包裝為不可變集合返回
return Collections.unmodifiableList(nestedArchives);
}
EntryIterator
首先是自身的迭代器灾前,通過內(nèi)部類EntryIterator
來實(shí)現(xiàn)展融,這里的邏輯很簡單不贅述。核心就一句話豫柬,entries
都是通過this.jarFile
獲取的告希。所以核心邏輯就在JarFile
類中 。
JarFile
基礎(chǔ)jdk的JarFile
進(jìn)行擴(kuò)展的子類烧给,類注釋上解釋說擴(kuò)展的功能有兩點(diǎn)燕偶。
- 獲取嵌套的jar中的任一目錄下的文件
- 獲取嵌套的jar中的jar文件
finally,到了解析自身jar最核心的邏輯了础嫡≈该矗看完JarFile類就能明白~酝惧!
JarFileEntry
JarFile中有一個(gè)很重要的類: JarFileEntry
,其類圖如下 :
首先是其實(shí)現(xiàn)了迭代接口伯诬,用于jar中entry的迭代遍歷晚唇。第二個(gè)比較重要的就是實(shí)現(xiàn)了中央目錄的Visitor
,這個(gè)是核心盗似。借助于CentralDirectoryParser
類哩陕,在RandomAccessData
(loader.dat
下的類,輔助數(shù)據(jù)讀群帐妗)的幫助下悍及,解析并遍歷了整個(gè)JarFile
中的文件,然后JarFileEntry
作為visitor被set
到CentralDirectoryParser
中接癌,也完成了整個(gè)JarFile中的文件的遍歷心赶,并將其緩存在entriesCache
中。entriesCache
是一個(gè)被同步的synchronizedMap
包裹的LinkedHashMap
缺猛。上文提到的EntryIterator
迭代數(shù)據(jù)其實(shí)就來自于這里的map
緩存的數(shù)據(jù)缨叫。
所以,loader的是如何解析jar中jar呢荔燎,還得繼續(xù)往前看耻姥,搞明白RandomAccessData
和CentralDirectoryParser
后,也許這次就真的弄明白了jar中jar的解析的代碼湖雹。
事實(shí)上這里其實(shí)才是整個(gè)loader項(xiàng)目中代碼量最大的地方。因?yàn)镴arFile牽扯到整個(gè)jar路徑和data路徑的所有類曙搬。其互相配合摔吏,相互調(diào)用,雖然看起來清晰纵装,但是要說明白還是要花點(diǎn)時(shí)間征讲,這周先到這里,下周繼續(xù)填坑橡娄。