SpringBoot java -jar 的啟動原理

SpringBoot java -jar 的啟動原理

電話面試中,面試官問了一個問題:你知道

java -jar

啟動 Spring Boot 專案,和傳統的 jar 有什麼不一樣的嗎?

問題大概是這樣,當時不太清楚怎麼回答,面試結束之後知道面試估計是掛了,請教了一下面試官這個問題應該從哪方面去考慮呢?

大概記得面試官說,。。。

自定義類載入器知道嗎

? 。。。(中間一些內容就沒聽進去了)

我:原來是從這方面去考慮呀,感謝面試官的指點!

事後趕緊學了學,也走讀了下啟動過程的原始碼,終於知道他說的自定義類載入器了,也就知道他問這個問題的目的所在了。

凡是你接觸過一點點 Spring Boot 專案,你一定知道透過

java -jar xxx。jar

命令便能把一個 Spring Boot 服務啟動起來。(如果你還沒接觸過,這裡的內容可以日後再看,先輕微瞭解一下 Spring Boot 專案的玩法)

一個看似簡陋的

java -jar

究竟幹了什麼,就把咱們手寫的應用(咱們的專案可能叫 XXXApplication。java)啟動了呢?

這就是本文的目的,解讀一下

java -jar

都做了什麼。

至少面試的時候能搭上話,能說兩句,不會像我一樣只能哦哦哦的。。。

溫馨提示:技術文章閱讀起來有些暈車,建議開啟寫作平臺給咱們提供的文章目錄進行閱讀

先有個概覽

瞭解一個技術點,直接扎到原始碼堆裡,雲裡霧裡,很難受,容易讓人望而生畏。

這時候可以先從整體或者非原始碼的角度瞭解一下它的運作機制,心裡有個底,如果再感興趣,就可以找一些細節,慢慢擊破,可能效果更好,更能讓人堅持下去。

這也是我後面準備學習原始碼的思路,就寫一下。

雖然也是這樣勸自己,可是還是看不懂,尷尬了,哈哈哈。。。

咱們就先拿這個

java -jar xxx。jar

來說:

Spring Boot 在可執行 Fat jar 包中定義了自己的一套規則,比如第三方依賴 jar 包在

/lib

目錄下,jar 包的 URL 路徑使用自定義的規則並且這個規則需要使用

org。springframework。boot。loader。jar。Handler

處理器處理。

Fat jar 的 Main-Class 使用

org。springframework。boot。loader。JarLauncher

,也就是 執行

java -jar xxx。jar

首先會觸發

JarLauncher

的 main 方法的執行,而不是咱們的應用的

xxx。xxx。xxx。XXXApplication

不過不用急,JarLauncher#main 會執行一些邏輯,做一些物料準備,最終會觸發咱們的 XXXApplication#main 啟動應用。

SpringBoot java -jar 的啟動原理

先看個啟動過程概覽,日後研究不會慌!

還不會畫時序圖,不搞個呢又感覺少了些直觀的東西,就勉強搞了個,這張圖的主要目的是提供啟動過程的呼叫關係。

SpringBoot java -jar 的啟動原理

怕時序圖表達不夠完善,再把簡要程式碼貼一下,哈哈。。。

SpringBoot java -jar 的啟動原理

提示:後面的東西需要一些耐心。

瞭解一些 Spring Boot 的抽象概念

瞭解一下 Spring Boot Loader 所抽象出來的一些概念,對走讀 Spring Boot loader 原始碼有些幫助

Launcher

:各種 Launcher 的基礎抽象類,用於啟動應用程式,跟 Archive 配合使用。

目前有 3 種實現,分別是

JarLauncher

WarLauncher

PropertiesLauncher

繼承關係如下

SpringBoot java -jar 的啟動原理

Archive

:歸檔檔案的基礎抽象類。

JarFileArchive 就是 jar 包檔案的抽象。

它提供了一些方法比如 getUrl 會返回這個 Archive 對應的 URL。getManifest 方法會獲得 Manifest 資料等。

ExplodedArchive 是檔案目錄的抽象。

JarFile

:對 jar 包的封裝,每個 JarFileArchive 都會對應一個 JarFile。JarFile 被構造的時候會解析內部結構,去獲取 jar 包裡的各個檔案或資料夾,這些檔案或資料夾會被封裝到 Entry 中,也儲存在 JarFileArchive 中。如果 Entry 是個 jar,會解析成 JarFileArchive。

JarFile 是 Springboot-loader 繼承 JDK

JarFile

提供的類。

比如一個 JarFileArchive 對應的 URL 為:

jar:file:C:\Users\Administrator\Desktop\demo\demo\target\jarlauncher-0。0。1-SNAPSHOT。jar!/

它對應的 JarFile 為:

C:\Users\Administrator\Desktop\demo\demo\target\jarlauncher-0。0。1-SNAPSHOT。jar

這個 JarFile 有很多 Entry,比如:

META-INF/META-INF/MANIFEST。MF……BOOT-INF/lib/spring-boot-starter-1。5。10。RELEASE。jarBOOT-INF/lib/spring-boot-1。5。10。RELEASE。jar。。。

JarFileArchive 內部的一些依賴 jar 對應的 URL

(SpringBoot 使用

org。springframework。boot。loader。jar。Handler

處理器來處理這些 URL)

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0。0。1-SNAPSHOT。jar!/lib/spring-boot-1。5。10。RELEASE。jar!/jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0。0。1-SNAPSHOT。jar!/lib/spring-boot-1。5。10。RELEASE。jar!/org/springframework/boot/loader/JarLauncher。class

我們看到如果有 jar 包中包含 jar,或者 jar 包中包含 jar 包裡面的 class 檔案,那麼會使用

!/

分隔開,這種方式只有 org。springframework。boot。loader。jar。Handler 能處理,它是 SpringBoot 內部擴展出來一種

URL 協議

其實這個非常重要,對於後面說的自定義載入器,拓展 URL 協議是基石

)。

可執行 jar 目錄結構

注意:咱們以 Spring Boot 1.5.10 版本來分析

本來想直接用 Spring Boot 2。3。x 作為 debug 環境的,也看了一圈網文,發現比 2。3。x 比 1。x 版本多了一些概念,比如分層的 JarModel,自己又不會,弄過來直接搪塞過去也不太好,就先放棄了,最終使用不算太老的 1。5。10 版本。

SpringBoot 提供了一個外掛 spring-boot-maven-plugin 用於把程式打包成一個

可執行的 jar 包

在 pom 檔案里加入這個外掛即可:

org。springframework。boot spring-boot-maven-plugin

然後我們在 Terminal 執行

maven package

打包完生成的 jarlauncher-0。0。1-SNAPSHOT。jar (我們稱之為 Fat jar)內部的結構如下:

├─BOOT-INF│ ├─classes│ │ └─application。properties│ │ └─com│ │ └─example│ │ └─jarlauncher│ │ └─JarlauncherApplication。class│ └─lib│ ├─spring-boot-1。5。10。RELEASE。jar│ ├─spring-boot-loader-1。5。10。RELEASE。jar│ ├─……。├─META-INF│ └─MANIFEST。MF│ └─maven│ └─com。example│ └─demo│ ├─pom。properties│ ├─pom。xml└─org └─springframework └─boot └─loader ├─ExecutableArchiveLauncher。class ├─JarLauncher。class ├─LaunchedURLClassLoader。class ├─Launcher。class ├─MainMethodRunner。class └─……

打包出來 fat jar 內部有三個資料夾:

META-INF 資料夾:程式入口,其中 MANIFEST。MF(資源清單) 用於描述 jar 包的資訊

BOOT-INF 目錄:放置我們的程式程式碼和第三方依賴的 jar 包

org 目錄:Spring Boot loader 相關的原始碼,我們程式啟動就靠他了

MANIFEST。MF 檔案的內容:

Manifest-Version: 1。0Implementation-Title: demoImplementation-Version: 0。0。1-SNAPSHOTArchiver-Version: Plexus ArchiverBuilt-By: AdministratorImplementation-Vendor-Id: com。exampleSpring-Boot-Version: 1。5。10。RELEASEImplementation-Vendor: Pivotal Software, Inc。Main-Class: org。springframework。boot。loader。JarLauncherStart-Class: com。example。jarlauncher。JarlauncherApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3。5。2Build-Jdk: 1。8。0_162Implementation-URL: http://projects。spring。io/spring-boot/demo/

我們看到,它的 Main-Class 是

org。springframework。boot。loader。JarLauncher

,當我們使用

java -jar

執行 jar 包的時候會呼叫 JarLauncher 的 main 方法,而不是呼叫我們編寫的

com。example。jarlauncher。JarlauncherApplication

接下來咱們走讀一下程式碼,看看實際怎麼執行的吧!

JarLauncher 的執行過程

提示:走讀的時候時不時結合概覽中的時序圖,可能好些。

JarLauncher 的 main 方法:

public static void main(String[] args) { // 構造JarLauncher,然後呼叫它的launch方法 new JarLauncher()。launch(args);}

JarLauncher 被構造的時候會呼叫父類 ExecutableArchiveLauncher 的構造方法。

ExecutableArchiveLauncher 的構造方法內部會去構造 Archive,這裡構造了 JarFileArchive。構造 JarFileArchive 的過程中還會構造很多東西,比如 JarFile,Entry …

public abstract class ExecutableArchiveLauncher extends Launcher { private final Archive archive; // 構造器會初始化代表 fat jar 的 Archive public ExecutableArchiveLauncher() { this。archive = createArchive(); } // 由父類 Launcher 實現 protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass()。getProtectionDomain(); CodeSource codeSource = protectionDomain。getCodeSource(); URI location = (codeSource == null ? null : codeSource。getLocation()。toURI()); String path = (location == null ? null : location。getSchemeSpecificPart()); 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); } // 最終會 new 一個 Arichive,內部生產的 JarFile——>這個逼對FatJar資源載入非常重要 return (root。isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); } @Override protected List getClassPathArchives() throws Exception { List archives = new ArrayList( // 獲取內部所有有的 Arichive this。archive。getNestedArchives(new EntryFilter() { @Override public boolean matches(Entry entry) { return isNestedArchive(entry); } })); // 空實現,沒用 postProcessClassPathArchives(archives); return archives; }}

JarLauncher 的 launch 方法:

protected void launch(String[] args) { try {// 在系統屬性中設定註冊了自定義的URL協議處理器:org。springframework。boot。loader。jar。Handler。// 初始化URL的時候,如果URL中沒有指定處理器,會去系統屬性中查詢 JarFile。registerUrlProtocolHandler();// getClassPathArchives方法會去找lib目錄下對應的第三方依賴JarFileArchive,同時也會找專案自身的JarFileArchive// 根據getClassPathArchives得到的JarFileArchive集合去建立類載入器ClassLoader。這裡會構造一個LaunchedURLClassLoader類載入器,這個類載入器繼承URLClassLoader,並使用這些JarFileArchive集合的URL構造成URLClassPath// 多說兩句句,// 1。URLClassPath這個屬性很重要,自定義ClassLoader,findClass就靠它了!// 2。可以關注一下構造LaunchedURLClassLoader時,archive。getUrl方法,這裡就涉及到自定義URL協議處理器了,JarFile等。畢竟實現jar in jar功能靠他們這些小羅羅。 ClassLoader classLoader = createClassLoader(getClassPathArchives());// getMainClass方法會去專案自身的Archive中的Manifest中找出key為Start-Class的類// 呼叫過載方法launch launch(args, getMainClass(), classLoader); } catch (Exception ex) { ex。printStackTrace(); System。exit(1); }}// Archive的getMainClass方法,不過由ExecutableArchiveLauncher實現// 這裡會找出Start-Class標識的com。example。jarlauncher。JarlauncherApplication這個類public String getMainClass() throws Exception { Manifest manifest = 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;}// launch過載方法protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 設定 LaunchedURLClassLoader 為執行緒上下文載入器 Thread。currentThread()。setContextClassLoader(classLoader); // 建立一個MainMethodRunner 並執行 createMainMethodRunner(mainClass, args, classLoader)。run();}

MainMethodRunner 的 run 方法:

public void run() throws Exception { // 使用執行緒上下文類載入器載入主類 Class<?> mainClass = Thread。currentThread()。getContextClassLoader() 。loadClass(this。mainClassName); // 反射執行,至此咱們的應用程式就啟動起來啦,good,啟動流程走讀結束,開心!可以跟面試官扯些了 Method mainMethod = mainClass。getDeclaredMethod(“main”, String[]。class); mainMethod。invoke(null, new Object[] { this。args });}

Start-Class 的 main 方法呼叫之後,內部會構造 Spring 容器,啟動內建 Servlet 容器等過程(後面的就不說了,不是本文關注的點,況且也沒細研究呢)

好了,到這裡咱們已經把 java -jar 的啟動過程整體瞭解了一遍,開心吧!

關於自定義的類載入器

看看傳說中的 LaunchedURLClassLoader 有什麼神奇的

LaunchedURLClassLoader 重寫了 loadClass 方法,走讀一下

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Handler。setUseFastConnectionExceptions(true); try { try { // 在呼叫 findClass 之前定義 package,確保巢狀JAR清單與包相關聯 definePackageIfNecessary(name); } catch (IllegalArgumentException ex) { if (getPackage(name) == null) { throw new AssertionError(“Package ” + name + “ has already been ” + “defined but it could not be found”); } } // 呼叫 父類 loadClass 走正常的載入委派流程 return super。loadClass(name, resolve); } finally { Handler。setUseFastConnectionExceptions(false); }}

其實只看上面 1。5。10 版本的 loadClass 實現,毫無亮點,基本就是普通的雙親委派過程。

而且 LaunchedURLClassLoader 使用的 findClass 是從父類 URLClassLoader 繼承的。

最終 loadClass 會走到 LaunchedURLClassLoader 的父類 URLClassLoader#findClass

protected Class<?> findClass(final String name) throws ClassNotFoundException{ final Class<?> result; try { result = AccessController。doPrivileged( new PrivilegedExceptionAction>() { public Class<?> run() throws ClassNotFoundException {// 把類名解析成路徑並加上。class字尾 String path = name。replace(‘。’, ‘/’)。concat(“。class”);// 基於之前得到的第三方jar包依賴以及自己的jar包得到URL陣列,進行遍歷找出對應類名的資源// 比如path是org/springframework/boot/loader/JarLauncher。class,它在jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1。0-SNAPSHOT。jar!/lib/spring-boot-loader-1。3。5。RELEASE。jar!/中被找出// 那麼找出的資源對應的URL為jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1。0-SNAPSHOT。jar!/lib/spring-boot-loader-1。3。5。RELEASE。jar!/org/springframework/boot/loader/JarLauncher。class // 載入fatjar class的關鍵部分!!! Resource res = ucp。getResource(path, false); if (res != null) { // 找到了資源 try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { throw new ClassNotFoundException(name); } } }, acc); } catch (java。security。PrivilegedActionException pae) { throw (ClassNotFoundException) pae。getException(); } if (result == null) { throw new ClassNotFoundException(name); } return result;}

上面的

findClass

的過程,都是在關鍵程式碼

Resource res = ucp。getResource(path, false);

這裡完成的。

ucp 也即 JDK 提供的

sun。misc。URLClassPath

又畫了個圖,可以看到

URLClassPath#getResource

涉及哪些基礎元件支援。

會用到 URL,URLStreamHandler,org。springframework。boot。loader。jar。Handler,最終獲取到 Resource,完成 class load。

SpringBoot java -jar 的啟動原理

所以,

個人結論:LaunchedURLClassLoader 是藉助他山之力,關鍵還在於 Spring Boot 對 URL jar 協議的拓展,Archeive,JarFile 的抽象

LaunchedURLClassLoader 載入測試

咱們手動模擬一下 JarLauncher 的載入過程,建立 LaunchedURLClassLoader,然後載入個類試試好不好使?

public class LaunchedURLClassLoaderTest { public static void main(String[] args) throws Exception { // 註冊org。springframework。boot。loader。jar。Handler URL協議處理器 JarFile。registerUrlProtocolHandler(); // 構造LaunchedURLClassLoader類載入器,這裡使用了1個URL,對應jar包中依賴包spring-boot-loader // 會使用 org。springframework。boot。loader。jar。Handler 處理器處理 LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader( new URL[]{ new URL(“jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0。0。1-SNAPSHOT。jar!/BOOT-INF/lib/spring-boot-loader-1。5。10。RELEASE。jar!/”) }, DemoApplication。class。getClassLoader()); // 載入類 classLoader。loadClass(“org。springframework。boot。loader。JarLauncher”); }}

把這個 case 跑通之後,JarLauncher 的啟動流程就沒啥問題了吧?

贈送一個 IDEA Debug Fat Jar 啟動的環境

說了這麼多啟動流程,如何才能直觀的 debug 到 Spring Boot Loader 的執行過程呢?

下面咱們就來做這事,很簡單,幾分鐘搞定。

程式碼準備

直接在 start。spring。io 初始化一個的 SpringBoot 應用就行,版本改成 1。5。10。

我這給個 GitHub 程式碼模板吧,點選去克隆

注意一點,maven 要新增 spring-boot-loader 的依賴,一起打到 jar 裡去。

<!—— Spring Boot loader ——> org。springframework。boot spring-boot-loader

然後

mvn package

,把應用打包成可執行 jar。

IDEA 配置

1、配置以 Jar 應用的方式啟動

SpringBoot java -jar 的啟動原理

2、配置 Jar 路徑,然後 Apply

SpringBoot java -jar 的啟動原理

3、找到啟動類 JarLauncher,打上斷點,debug 方式啟動

SpringBoot java -jar 的啟動原理

如果你覺得這篇文章對你有幫助 點贊關注,然後私信回覆【888】即可獲取Java進階全套影片以及原始碼學習資料

SpringBoot java -jar 的啟動原理

SpringBoot java -jar 的啟動原理