1. 程式人生 > 其它 >SpringBoot FatJar啟動原理分析

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的方法
  1. 實現URLStreamHandlerFactory介面,通過方法URL.setURLStreamHandlerFactory設定。該屬性是一個靜態屬性,且只能被設定一次。
  2. 直接提供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>
進行打包,打包後的檔案佈局如下:
  1. BOOT-INF資料夾下放的程式編譯class和依賴的jar包
  2. 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個事情:
  1. 以FatJar為file作為入參,構造JarFileArchive物件。獲取其中所有的資源目標,取得其Url,將這些URL作為引數,構建了一個URLClassLoader。
  2. 以第一步構建的ClassLoader載入MANIFEST.MF檔案中Start-Class
  3. 指向的業務類,並且執行靜態方法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定製了以下兩個方面:
  1. 實現了一個java.net.URLStreamHandler的子類org.springframework.boot.loader.jar.Handler。該Handler支援識別多個!/分隔符,並且正確的開啟URLConnection。開啟的Connection是SpringBoot定製的org.springframework.boot.loader.jar.JarURLConnection實現。
  2. 實現了一個java.net.JarURLConnection的子類org.springframework.boot.loader.jar.JarURLConnection。該連結支援多個!/分隔符,並且自己實現了在這種情況下獲取InputStream的方法。而為了能夠在org.springframework.boot.loader.jar.JarURLConnection
  3. 正確獲取輸入流,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解析可以在載入類階段避免頻繁的檔案解壓動作,在效能上良好一些。