SpringBoot FatJar啟動原理
背景
本文會探究下SpringBoot的啟動原理。SpringBoot在打包的時候會將依賴包也打進最終的Jar,變成一個可運行的FatJar。也就是會形成一個Jar in Jar的結構。默認情況下,JDK提供的ClassLoader只能識別Jar中的class文件以及加載classpath下的其他jar包中的class文件。對於在jar包中的jar包是無法加載的。
儲備知識
URLStreamHandler
java中描述資源常使用URL。而URL有一個方法用於打開鏈接java.net.URL#openConnection()
。由於URL用於表達各種各樣的資源,打開資源的具體動作由java.net.URLStreamHandler
這個類的子類來完成。根據不同的協議,會有不同的handler實現。而JDK內置了相當多的handler實現用於應對不同的協議。比如jar
、file
、http
等等。URL內部有一個靜態HashTable
屬性,用於保存已經被發現的協議和handler實例的映射。
獲得URLStreamHandler
有三種方法
- 實現
URLStreamHandlerFactory
接口,通過方法URL.setURLStreamHandlerFactory
設置。該屬性是一個靜態屬性,且只能被設置一次。 - 直接提供
URLStreamHandler
的子類,作為URL的構造方法的入參之一。但是在JVM中有固定的規范要求:- 子類的類名必須是 Handler ,同時最后一級的包名必須是協議的名稱。比如自定義了Http的協議實現,則類名必然為xx.http.Handler
- JVM 啟動的時候,需要設置
java.protocol.handler.pkgs
系統屬性,如果有多個實現類,那么中間用 | 隔開。因為JVM在嘗試尋找Handler時,會從這個屬性中獲取包名前綴,最終使用包名前綴.協議名.Handler
,使用Class.forName
方法嘗試初始化類,如果初始化成功,則會使用該類的實現作為協議實現。
Archive
SpringBoot定義了一個接口用於描述資源,也就是org.springframework.boot.loader.archive.Archive
。該接口有兩個實現,分別是org.springframework.boot.loader.archive.ExplodedArchive
和org.springframework.boot.loader.archive.JarFileArchive
。前者用於在文件夾目錄下尋找資源,后者用於在jar包環境下尋找資源。而在SpringBoot打包的fatJar中,則是使用后者。
打包
SpringBoot使用插件
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.tccdemo.Eureka</mainClass>
</configuration>
</plugin>
進行打包,打包后的文件布局如下:
- BOOT-INF文件夾下放的程序編譯class和依賴的jar包
- org目錄下放的是SpringBoot的啟動相關包。
來看描述文件MANIFEST.MF
的內容
Manifest-Version: 1.0
Implementation-Title: eureka
Implementation-Version: 1.0-SNAPSHOT
Built-By: Administrator
Implementation-Vendor-Id: com.tccdemo
Spring-Boot-Version: 2.0.2.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.tccdemo.Eureka
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_201
Implementation-URL: http://www.example.com
最為顯眼的就是程序的啟動類並不是我們項目的啟動類,而是SpringBoot的JarLauncher
。下面會來深究下這個類的作用。
SpringBoot啟動
首先來看啟動方法
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
JarLauncher
繼承於org.springframework.boot.loader.ExecutableArchiveLauncher
。該類的無參構造方法最主要的功能就是構建了當前main方法所在的FatJar的JarFileArchive
對象。下面來看launch方法。該方法主要是做了2個事情:
- 以FatJar為file作為入參,構造JarFileArchive對象。獲取其中所有的資源目標,取得其Url,將這些URL作為參數,構建了一個URLClassLoader。
- 以第一步構建的ClassLoader加載
MANIFEST.MF
文件中Start-Class
指向的業務類,並且執行靜態方法main。進而啟動整個程序。
通過靜態方法org.springframework.boot.loader.JarLauncher#main
就可以順利啟動整個程序。這里面的關鍵在於SpringBoot自定義的classLoader能夠識別FatJar中的資源,包括有:在指定目錄下的項目編譯class、在指令目錄下的項目依賴jar。JDK默認用於加載應用的AppClassLoader只能從jar的根目錄開始加載class文件,並且也不支持jar in jar這種格式。
為了實現這個目標,SpringBoot首先從支持jar in jar中內容讀取做了定制,也就是支持多個!/
分隔符的url路徑。SpringBoot定制了以下兩個方面:
- 實現了一個
java.net.URLStreamHandler
的子類org.springframework.boot.loader.jar.Handler
。該Handler支持識別多個!/
分隔符,並且正確的打開URLConnection
。打開的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection
實現。 - 實現了一個
java.net.JarURLConnection
的子類org.springframework.boot.loader.jar.JarURLConnection
。該鏈接支持多個!/
分隔符,並且自己實現了在這種情況下獲取InputStream的方法。而為了能夠在org.springframework.boot.loader.jar.JarURLConnection
正確獲取輸入流,SpringBoot自定義了一套讀取ZipFile的工具類和方法。這部分和ZIP壓縮算法規范緊密相連,就不深入了。
能夠讀取多個!/
的url后,事情就變得很簡單了。上文提到的ExecutableArchiveLauncher
的launch
方法會以當前的FatJar構建一個JarFileArchive
,並且通過該對象獲取其內部所有的資源URL,這些URL包含項目編譯class和依賴jar包。在構建這些URL的時候傳入的就是SpringBoot定制的Handler。將獲取的URL數組作為參數傳遞給自定義的ClassLoaderorg.springframework.boot.loader.LaunchedURLClassLoader
。該ClassLoader繼承自UrlClassLoader。UrlClassLoader加載class就是依靠初始參數傳入的Url數組,並且嘗試Url指向的資源中加載Class文件。有了自定義的Handler,再從Url中嘗試獲取資源就變得很容易了。
至此,SpringBoot自定義的ClassLoader就能夠加載FatJar中的依賴包的class文件了。
擴展
SpringBoot提供了一個很好的思路,但是其內部實現非常復雜,特別是其自行實現了一個ZipFIle的解析器。但是本質上這些背后的工作都是為了能夠讀取到FatJar內部的Jar的class文件資源。也就是只要有辦法能夠讀取這些資源其實就可以實現加載Class文件了。而依靠JDK本身提供的JarFile其實就可以做到了。而讀取到所有資源后,自定義一個ClassLoader加載讀取到二進制數據進而定義Class對象並不是很難的項目實現。當然,SpringBoot定制的Zip解析可以在加載類階段避免頻繁的文件解壓動作,在性能上良好一些。
文章原創首發於公眾號:林斌說Java,轉載請注明來源,謝謝。
歡迎掃碼關注