javascript
硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
摘要: 原創(chuàng)出處 http://www.iocoder.cn/Spring-Boot/jar/ 「芋道源碼」歡迎轉(zhuǎn)載,保留摘要,謝謝!
-
1. 概述
-
2. MANIFEST.MF
-
3. JarLauncher
-
4. LaunchedURLClassLoader
-
666. 彩蛋
大家好,我是艿艿,一個(gè)熬夜退役選手~
這兩周很不順心,接連的變化,一臉的懵逼。還好我還有 150000 女粉,我又充滿能量的肝完了本文。
1. 概述
Spring Boot 提供了 Maven 插件?spring-boot-maven-plugin,可以方便的將 Spring Boot 項(xiàng)目打成?jar?包或者?war?包。
考慮到部署的便利性,我們絕大多數(shù) 99.99% 的場(chǎng)景下,我們會(huì)選擇打成?jar?包。這樣,我們就無需在部署項(xiàng)目的服務(wù)器上,配置相應(yīng)的 Tomcat、Jetty 等 Servlet 容器。
那么,jar?包是如何運(yùn)行,并啟動(dòng) Spring Boot 項(xiàng)目的呢?這個(gè)就是本文的目的,一起弄懂 Spring Boot?jar?包的運(yùn)行原理。
下面,我們來打開一個(gè) Spring Boot?jar?包,看看其里面的結(jié)構(gòu)。如下圖所示,一共分成四部分:
Spring Boot?jar?包?
-
①?META-INF?目錄:通過?MANIFEST.MF?文件提供?jar?包的元數(shù)據(jù),聲明了?jar?的啟動(dòng)類。
-
②?org?目錄:為 Spring Boot 提供的?spring-boot-loader?項(xiàng)目,它是?java -jar?啟動(dòng) Spring Boot 項(xiàng)目的秘密所在,也是稍后我們將深入了解的部分。
“Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using?java -jar. Generally you will not need to use?spring-boot-loader?directly, but instead work with the Gradle or Maven plugin.
-
③?BOOT-INF/lib?目錄:我們 Spring Boot 項(xiàng)目中引入的依賴的?jar?包們。spring-boot-loader?項(xiàng)目很大的一個(gè)作用,就是解決?jar?包里嵌套?jar?的情況,如何加載到其中的類。
-
④?BOOT-INF/classes?目錄:我們?cè)?Spring Boot 項(xiàng)目中 Java 類所編譯的?.class、配置文件等等。
先簡(jiǎn)單劇透下,spring-boot-loader?項(xiàng)目需要解決兩個(gè)問題:
-
第一,如何引導(dǎo)執(zhí)行我們創(chuàng)建的 Spring Boot 應(yīng)用的啟動(dòng)類,例如上述圖中的 Application 類。
-
第二,如何加載?BOOT-INF/class?目錄下的類,以及?BOOT-INF/lib?目錄下內(nèi)嵌的?jar?包中的類。
下面,尾隨艿艿,一起來抽絲剝繭!
2. MANIFEST.MF
我們來查看?META-INF/MANIFEST.MF?文件,里面的內(nèi)容如下:
Manifest-Version: 1.0 Implementation-Title: lab-39-demo Implementation-Version: 2.2.2.RELEASE Start-Class: cn.iocoder.springboot.lab39.skywalkingdemo.Application Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.2.2.RELEASE Created-By: Maven Archiver 3.4.0 Main-Class: org.springframework.boot.loader.JarLauncher它實(shí)際是一個(gè)?Properties?配置文件,每一行都是一個(gè)配置項(xiàng)目。重點(diǎn)來看看兩個(gè)配置項(xiàng):
-
Main-Class?配置項(xiàng):Java 規(guī)定的?jar?包的啟動(dòng)類,這里設(shè)置為?spring-boot-loader?項(xiàng)目的 JarLauncher 類,進(jìn)行 Spring Boot 應(yīng)用的啟動(dòng)。
-
Start-Class?配置項(xiàng):Spring Boot 規(guī)定的主啟動(dòng)類,這里設(shè)置為我們定義的 Application 類。
小知識(shí)補(bǔ)充:為什么會(huì)有?Main-Class/Start-Class?配置項(xiàng)呢?因?yàn)槲覀兪峭ㄟ^ Spring Boot 提供的 Maven 插件?spring-boot-maven-plugin?進(jìn)行打包,該插件將該配置項(xiàng)寫入到?MANIFEST.MF?中,從而能讓?spring-boot-loader?能夠引導(dǎo)啟動(dòng) Spring Boot 應(yīng)用。
可能胖友會(huì)有疑惑,Start-Class?對(duì)應(yīng)的 Application 類自帶了?#main(String[] args)?方法,為什么我們不能直接運(yùn)行會(huì)如何呢?我們來簡(jiǎn)單嘗試一下哈,控制臺(tái)執(zhí)行如下:
$?java?-classpath?lab-39-demo-2.2.2.RELEASE.jar?cn.iocoder.springboot.lab39.skywalkingdemo.Application 錯(cuò)誤:?找不到或無法加載主類?cn.iocoder.springboot.lab39.skywalkingdemo.Application直接找不到 Application 類,因?yàn)樗?BOOT-INF/classes?目錄下,不符合 Java 默認(rèn)的?jar?包的加載規(guī)則。因此,需要通過 JarLauncher 啟動(dòng)加載。
當(dāng)然實(shí)際還有一個(gè)更重要的原因,Java 規(guī)定可執(zhí)行器的?jar?包禁止嵌套其它?jar?包。但是我們可以看到?BOOT-INF/lib?目錄下,實(shí)際有 Spring Boot 應(yīng)用依賴的所有?jar?包。因此,spring-boot-loader?項(xiàng)目自定義實(shí)現(xiàn)了 ClassLoader 實(shí)現(xiàn)類 LaunchedURLClassLoader,支持加載?BOOT-INF/classes?目錄下的?.class?文件,以及?BOOT-INF/lib?目錄下的?jar?包。
3. JarLauncher
JarLauncher 類是針對(duì) Spring Boot?jar?包的啟動(dòng)類,整體類圖如下所示:
JarLauncher 類圖?
“友情提示:WarLauncher 類,是針對(duì) Spring Boot?war?包的啟動(dòng)類,后續(xù)胖友可以自己瞅瞅,差別并不大哈~
JarLauncher 的源碼比較簡(jiǎn)單,如下圖所示:
public?class?JarLauncher?extends?ExecutableArchiveLauncher?{static?final?String?BOOT_INF_CLASSES?=?"BOOT-INF/classes/";static?final?String?BOOT_INF_LIB?=?"BOOT-INF/lib/";public?JarLauncher()?{}protected?JarLauncher(Archive?archive)?{super(archive);}@Overrideprotected?boolean?isNestedArchive(Archive.Entry?entry)?{if?(entry.isDirectory())?{return?entry.getName().equals(BOOT_INF_CLASSES);}return?entry.getName().startsWith(BOOT_INF_LIB);}public?static?void?main(String[]?args)?throws?Exception?{new?JarLauncher().launch(args);}}通過?#main(String[] args)?方法,創(chuàng)建 JarLauncher 對(duì)象,并調(diào)用其?#launch(String[] args)?方法進(jìn)行啟動(dòng)。整體的啟動(dòng)邏輯,其實(shí)是由父類 Launcher 所提供,如下圖所示:
Launcher 啟動(dòng)過程父類 Launcher 的?#launch(String[] args)?方法,代碼如下:
//?Launcher.javaprotected?void?launch(String[]?args)?throws?Exception?{//?<1>?注冊(cè)?URL?協(xié)議的處理器JarFile.registerUrlProtocolHandler();//?<2>?創(chuàng)建類加載器ClassLoader?classLoader?=?createClassLoader(getClassPathArchives());//?<3>?執(zhí)行啟動(dòng)類的?main?方法launch(args,?getMainClass(),?classLoader); }-
<1>?處,調(diào)用 JarFile 的?#registerUrlProtocolHandler()?方法,注冊(cè) Spring Boot 自定義的 URLStreamHandler 實(shí)現(xiàn)類,用于?jar?包的加載讀取。
-
<2>?處,調(diào)用自身的?#createClassLoader(List<Archive> archives)?方法,創(chuàng)建自定義的 ClassLoader 實(shí)現(xiàn)類,用于從?jar?包中加載類。
-
<3>?處,執(zhí)行我們聲明的 Spring Boot 啟動(dòng)類,進(jìn)行 Spring Boot 應(yīng)用的啟動(dòng)。
簡(jiǎn)單來說,就是整一個(gè)可以讀取?jar?包中類的加載器,保證?BOOT-INF/lib?目錄下的類和?BOOT-classes?內(nèi)嵌的?jar?中的類能夠被正常加載到,之后執(zhí)行 Spring Boot 應(yīng)用的啟動(dòng)。
下面,我們逐行代碼來看看噢。即將代碼多多,保持淡定,嘿嘿~
3.1 registerUrlProtocolHandler
“友情提示:對(duì)應(yīng)?JarFile.registerUrlProtocolHandler();?代碼段,不要迷路。
JarFile 是?java.util.jar.JarFile?的子類,如下所示:
public?class?JarFile?extends?java.util.jar.JarFile?{//?...?省略其它代碼}JarFile 主要增強(qiáng)支持對(duì)內(nèi)嵌的?jar?包的獲取。如下圖所示:
讀取內(nèi)嵌的?jar?包的演示OK,介紹完之后,讓我們回到 JarFile 的?#registerUrlProtocolHandler()?方法,注冊(cè) Spring Boot 自定義的 URL 協(xié)議的處理器。代碼如下:
//?JarFile.javaprivate?static?final?String?PROTOCOL_HANDLER?=?"java.protocol.handler.pkgs";private?static?final?String?HANDLERS_PACKAGE?=?"org.springframework.boot.loader";/***?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()?{//?獲得?URLStreamHandler?的路徑String?handlers?=?System.getProperty(PROTOCOL_HANDLER,?"");//?將?Spring?Boot?自定義的?HANDLERS_PACKAGE(org.springframework.boot.loader)?補(bǔ)充上去System.setProperty(PROTOCOL_HANDLER,?("".equals(handlers)???HANDLERS_PACKAGE:?handlers?+?"|"?+?HANDLERS_PACKAGE));//?重置已緩存的?URLStreamHandler?處理器們r(jià)esetCachedUrlHandlers(); }/***?Reset?any?cached?handlers?just?in?case?a?jar?protocol?has?already?been?used.*?We?reset?the?handler?by?trying?to?set?a?null?{@link?URLStreamHandlerFactory}?which*?should?have?no?effect?other?than?clearing?the?handlers?cache.**?重置?URL?中的?URLStreamHandler?的緩存,防止?`jar://`?協(xié)議對(duì)應(yīng)的?URLStreamHandler?已經(jīng)創(chuàng)建*?我們通過設(shè)置?URLStreamHandlerFactory?為?null?的方式,清空?URL?中的該緩存。*/ private?static?void?resetCachedUrlHandlers()?{try?{URL.setURLStreamHandlerFactory(null);}?catch?(Error?ex)?{//?Ignore} }-
胖友先跟著注釋,自己閱讀下如上的代碼~
目的很明確,通過將?org.springframework.boot.loader?包設(shè)置到?"java.protocol.handler.pkgs"?環(huán)境變量,從而使用到自定義的 URLStreamHandler 實(shí)現(xiàn)類 Handler,處理?jar:?協(xié)議的 URL。
“友情提示:這里我們暫時(shí)不深入 Handler 的源碼,避免直接走的太深,丟失了主干。后續(xù)胖友可結(jié)合《Java URL 協(xié)議擴(kuò)展實(shí)現(xiàn)》文章,進(jìn)行 Handler 的實(shí)現(xiàn)理解。
另外,HandlerTests 提供的單元測(cè)試,也是非常有幫助的~
3.2 createClassLoader
“友情提示:對(duì)應(yīng)?ClassLoader classLoader = createClassLoader(getClassPathArchives())?代碼段,不要迷路。
3.2.1 getClassPathArchives
首先,我們先來看看?#getClassPathArchives()?方法,它是由 ExecutableArchiveLauncher 所實(shí)現(xiàn),代碼如下:
//?ExecutableArchiveLauncher.javaprivate?final?Archive?archive;@Override protected?List<Archive>?getClassPathArchives()?throws?Exception?{//?<1>?獲得所有?ArchiveList<Archive>?archives?=?new?ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));//?<2>?后續(xù)處理postProcessClassPathArchives(archives);return?archives; }protected?abstract?boolean?isNestedArchive(Archive.Entry?entry);protected?void?postProcessClassPathArchives(List<Archive>?archives)?throws?Exception?{ } “友情提示:這里我們會(huì)看到一個(gè) Archive 對(duì)象,先可以暫時(shí)理解成一個(gè)一個(gè)的檔案,稍后會(huì)清晰認(rèn)識(shí)的~
<1>?處,this::isNestedArchive?代碼段,創(chuàng)建了 EntryFilter 匿名實(shí)現(xiàn)類,用于過濾?jar?包不需要的目錄。
//?Archive.java/***?Represents?a?single?entry?in?the?archive.*/ interface?Entry?{/***?Returns?{@code?true}?if?the?entry?represents?a?directory.*?@return?if?the?entry?is?a?directory*/boolean?isDirectory();/***?Returns?the?name?of?the?entry.*?@return?the?name?of?the?entry*/String?getName();}/***?Strategy?interface?to?filter?{@link?Entry?Entries}.*/ interface?EntryFilter?{/***?Apply?the?jar?entry?filter.*?@param?entry?the?entry?to?filter*?@return?{@code?true}?if?the?filter?matches*/boolean?matches(Entry?entry);}這里在它的內(nèi)部,調(diào)用了?#isNestedArchive(Archive.Entry entry)?方法,它是由 JarLauncher 所實(shí)現(xiàn),代碼如下:
//?JarLauncher.javastatic?final?String?BOOT_INF_CLASSES?=?"BOOT-INF/classes/";static?final?String?BOOT_INF_LIB?=?"BOOT-INF/lib/";@Override protected?boolean?isNestedArchive(Archive.Entry?entry)?{//?如果是目錄的情況,只要?BOOT-INF/classes/?目錄if?(entry.isDirectory())?{return?entry.getName().equals(BOOT_INF_CLASSES);}//?如果是文件的情況,只要?BOOT-INF/lib/?目錄下的?`jar`?包return?entry.getName().startsWith(BOOT_INF_LIB); }-
目的就是過濾獲得,BOOT-INF/classes/?目錄下的類,以及?BOOT-INF/lib/?的內(nèi)嵌?jar?包。
<1>?處,this.archive.getNestedArchives?代碼段,調(diào)用 Archive 的?#getNestedArchives(EntryFilter filter)?方法,獲得?archive?內(nèi)嵌的 Archive 集合。代碼如下:
//?Archive.java/***?Returns?nested?{@link?Archive}s?for?entries?that?match?the?specified?filter.*?@param?filter?the?filter?used?to?limit?entries*?@return?nested?archives*?@throws?IOException?if?nested?archives?cannot?be?read*/ List<Archive>?getNestedArchives(EntryFilter?filter)?throws?IOException;Archive 接口,是?spring-boot-loader?項(xiàng)目定義的檔案抽象,其子類如下圖所示:
Archive 類圖?
-
ExplodedArchive 是針對(duì)目錄的 Archive 實(shí)現(xiàn)類。
-
JarFileArchive 是針對(duì)?jar?包的 Archive 實(shí)現(xiàn)類。
友情提示:這塊可能有一丟丟復(fù)雜,胖友吃耐心哈~
那么,我們?cè)?ExecutableArchiveLauncher 的?archive?屬性是怎么來的呢?答案在 ExecutableArchiveLauncher 的構(gòu)造方法中,代碼如下:
//?ExecutableArchiveLauncher.javapublic?abstract?class?ExecutableArchiveLauncher?extends?Launcher?{private?final?Archive?archive;public?ExecutableArchiveLauncher()?{try?{this.archive?=?createArchive();}?catch?(Exception?ex)?{throw?new?IllegalStateException(ex);}}protected?ExecutableArchiveLauncher(Archive?archive)?{this.archive?=?archive;}//?...?省略其它 }//?Launcher.java public?abstract?class?Launcher?{protected?final?Archive?createArchive()?throws?Exception?{//?獲得?jar?所在的絕對(duì)路徑ProtectionDomain?protectionDomain?=?getClass().getProtectionDomain();CodeSource?codeSource?=?protectionDomain.getCodeSource();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");}File?root?=?new?File(path);if?(!root.exists())?{throw?new?IllegalStateException("Unable?to?determine?code?source?archive?from?"?+?root);}//?如果是目錄,則使用?ExplodedArchive?進(jìn)行展開//?如果不是目錄,則使用?JarFileArchivereturn?(root.isDirectory()???new?ExplodedArchive(root):?new?JarFileArchive(root));}}根據(jù)根路徑是否為目錄的情況,創(chuàng)建 ExplodedArchive 或 JarFileArchive 對(duì)象。那么問題就來了,這里的?root?是什么呢?艿艿一波騷操作,終于輸出了答案,如下圖所示:
root?是什么!root?路徑為?jar?包的絕對(duì)地址,也就是說創(chuàng)建 JarFileArchive 對(duì)象。原因是,Launcher 所在包為?org?下,它的根目錄當(dāng)然是?jar?包的絕對(duì)路徑哈!
😈 現(xiàn)在是不是對(duì) Archive 稍微有點(diǎn)感覺落?繼續(xù)附加如下代碼,打印 JarFileArchive 的?#getNestedArchives(EntryFilter filter)?方法的執(zhí)行結(jié)果。
//?==========?附加代碼: //?創(chuàng)建?Archive?對(duì)象 Archive?archive?=?new?JarFileArchive(root); //?創(chuàng)建?EntryFilter?對(duì)象 Archive.EntryFilter?filter?=?new?Archive.EntryFilter()?{static?final?String?BOOT_INF_CLASSES?=?"BOOT-INF/classes/";static?final?String?BOOT_INF_LIB?=?"BOOT-INF/lib/";@Overridepublic?boolean?matches(Archive.Entry?entry)?{//?如果是目錄的情況,只要?BOOT-INF/classes/?目錄if?(entry.isDirectory())?{return?entry.getName().equals(BOOT_INF_CLASSES);}//?如果是文件的情況,只要?BOOT-INF/lib/?目錄下的?`jar`?包return?entry.getName().startsWith(BOOT_INF_LIB);}}; //?執(zhí)行讀取 for?(Archive?item?:?archive.getNestedArchives(filter))?{System.out.println(item.getUrl()); }//?==========?執(zhí)行結(jié)果: jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/classes!/ jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.2.RELEASE.jar!/ jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-starter-2.2.2.RELEASE.jar!/ jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-2.2.2.RELEASE.jar!/ ...?省略其他?jar?包從執(zhí)行結(jié)果可以看出,BOOT-INF/classes/?目錄被歸類為一個(gè)?Archive 對(duì)象,而?BOOT-INF/lib/?目錄下的每個(gè)內(nèi)嵌?jar?包都對(duì)應(yīng)一個(gè)?Archive 對(duì)象。
來來來,回過頭來看看 JarFileArchive 的?#getNestedArchives(EntryFilter filter)?方法的源碼,如下圖所示:
JarFileArchive 的?#getNestedArchives(EntryFilter filter)?方法現(xiàn)在是不是明白了噢!良心如我,哈哈哈!
“友情提示:上述的測(cè)試代碼,可以訪問 lab-39-demo 項(xiàng)目查看。
3.2.2 createClassLoader
然后,我再來看看?#createClassLoader(List<Archive> archives)?方法,它是由 ExecutableArchiveLauncher 所實(shí)現(xiàn),代碼如下:
//?ExecutableArchiveLauncher.javaprotected?ClassLoader?createClassLoader(List<Archive>?archives)?throws?Exception?{//?獲得所有?Archive?的?URL?地址List<URL>?urls?=?new?ArrayList<>(archives.size());for?(Archive?archive?:?archives)?{urls.add(archive.getUrl());}//?創(chuàng)建加載這些?URL?的?ClassLoaderreturn?createClassLoader(urls.toArray(new?URL[0])); }protected?ClassLoader?createClassLoader(URL[]?urls)?throws?Exception?{return?new?LaunchedURLClassLoader(urls,?getClass().getClassLoader()); }基于獲得的 Archive 數(shù)組,創(chuàng)建自定義 ClassLoader 實(shí)現(xiàn)類 LaunchedURLClassLoader,通過它來加載?BOOT-INF/classes?目錄下的類,以及?BOOT-INF/lib?目錄下的?jar?包中的類。
進(jìn)一步的解析,我們?cè)凇?. LaunchedURLClassLoader」小節(jié)中,進(jìn)行分享哈!
3.3 launch
“友情提示:對(duì)應(yīng)?launch(args, getMainClass(), classLoader)?代碼段,不要迷路。
3.3.1 getMainClass
首先,我們先來看看#getMainClass()?方法,它是由 ExecutableArchiveLauncher 所實(shí)現(xiàn),代碼如下:
//?ExecutableArchiveLauncher.java@Override protected?String?getMainClass()?throws?Exception?{//?獲得啟動(dòng)的類的全名Manifest?manifest?=?this.archive.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; }從?jar?包的?MANIFEST.MF?文件的?Start-Class?配置項(xiàng),,獲得我們?cè)O(shè)置的 Spring Boot 的主啟動(dòng)類。
3.3.2 createMainMethodRunner
然后,我們?cè)賮砜纯?#launch()?方法,它是由 Launcher 所實(shí)現(xiàn),代碼如下:
protected?void?launch(String[]?args,?String?mainClass,?ClassLoader?classLoader)throws?Exception?{//?<1>?設(shè)置?LaunchedURLClassLoader?作為類加載器Thread.currentThread().setContextClassLoader(classLoader);//?<2>?創(chuàng)建?MainMethodRunner?對(duì)象,并執(zhí)行?run?方法,啟動(dòng)?Spring?Boot?應(yīng)用createMainMethodRunner(mainClass,?args,?classLoader).run(); }該方法負(fù)責(zé)最終的 Spring Boot 應(yīng)用真正的啟動(dòng)。
-
<1>?處:設(shè)置「3.2.2 createClassLoader」創(chuàng)建的 LaunchedURLClassLoader 作為類加載器,從而保證能夠從?jar?加載到相應(yīng)的類。
-
<2>?處,調(diào)用?#createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader)?方法,創(chuàng)建 MainMethodRunner 對(duì)象,并執(zhí)行其?#run()?方法來啟動(dòng) Spring Boot 應(yīng)用。
下面,我們來看看?MainMethodRunner?類,負(fù)責(zé) Spring Boot 應(yīng)用的啟動(dòng)。代碼如下:
public?class?MainMethodRunner?{private?final?String?mainClassName;private?final?String[]?args;/***?Create?a?new?{@link?MainMethodRunner}?instance.*?@param?mainClass?the?main?class*?@param?args?incoming?arguments*/public?MainMethodRunner(String?mainClass,?String[]?args)?{this.mainClassName?=?mainClass;this.args?=?(args?!=?null)???args.clone()?:?null;}public?void?run()?throws?Exception?{//?<1>?加載?Spring?BootClass<?>?mainClass?=?Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);//?<2>?反射調(diào)用?main?方法Method?mainMethod?=?mainClass.getDeclaredMethod("main",?String[].class);mainMethod.invoke(null,?new?Object[]?{?this.args?});}}-
<1>?處:通過 LaunchedURLClassLoader 類加載器,加載到我們?cè)O(shè)置的 Spring Boot 的主啟動(dòng)類。
-
<2>?處:通過反射調(diào)用主啟動(dòng)類的?#main(String[] args)?方法,啟動(dòng) Spring Boot 應(yīng)用。這里也告訴了我們答案,為什么我們通過編寫一個(gè)帶有?#main(String[] args)?方法的類,就能夠啟動(dòng) Spring Boot 應(yīng)用。
4. LaunchedURLClassLoader
LaunchedURLClassLoader 是?spring-boot-loader?項(xiàng)目自定義的類加載器,實(shí)現(xiàn)對(duì)?jar?包中?META-INF/classes?目錄下的類和?META-INF/lib?內(nèi)嵌的?jar?包中的類的加載。
“FROM 《維基百科 —— Java 類加載器》
Java 類加載器是 Java 運(yùn)行時(shí)環(huán)境的一個(gè)部件,負(fù)責(zé)動(dòng)態(tài)加載 Java 類到 Java 虛擬機(jī)的內(nèi)存空間中。類通常是按需加載,即第一次使用該類時(shí)才加載。
由于有了類加載器,Java 運(yùn)行時(shí)系統(tǒng)不需要知道文件與文件系統(tǒng)。對(duì)學(xué)習(xí)類加載器而言,掌握 Java 的委派概念是很重要的。每個(gè) Java 類必須由某個(gè)類加載器裝入到內(nèi)存。
在「3.2.2 createClassLoader」小節(jié)中,我們可以看到 LaunchedURLClassLoader 的創(chuàng)建代碼如下:
//?ExecutableArchiveLauncher.javaprotected?ClassLoader?createClassLoader(List<Archive>?archives)?throws?Exception?{//?獲得所有?Archive?的?URL?地址List<URL>?urls?=?new?ArrayList<>(archives.size());for?(Archive?archive?:?archives)?{urls.add(archive.getUrl());}//?創(chuàng)建加載這些?URL?的?ClassLoaderreturn?createClassLoader(urls.toArray(new?URL[0])); }protected?ClassLoader?createClassLoader(URL[]?urls)?throws?Exception?{return?new?LaunchedURLClassLoader(urls,?getClass().getClassLoader()); }//?LaunchedURLClassLoader.javapublic?class?LaunchedURLClassLoader?extends?URLClassLoader?{public?LaunchedURLClassLoader(URL[]?urls,?ClassLoader?parent)?{super(urls,?parent);}}-
第一個(gè)參數(shù)?urls,使用的是 Archive 集合對(duì)應(yīng)的 URL 地址們,從而告訴 LaunchedURLClassLoader 讀取?jar?的地址。
-
第二個(gè)參數(shù)?parent,設(shè)置 LaunchedURLClassLoader 的父加載器。這里后續(xù)胖友可以理解下,類加載器的雙親委派模型,這里就拓展開了。
LaunchedURLClassLoader 的實(shí)現(xiàn)代碼并不多,我們主要來看看它是如何從?jar?包中加載類的。核心如下圖所示:
-
<1>?處,在通過父類的?#getPackage(String name)?方法獲取不到指定類所在的包時(shí),會(huì)通過遍歷?urls?數(shù)組,從?jar?包中加載類所在的包。當(dāng)找到包時(shí),會(huì)調(diào)用?#definePackage(String name, Manifest man, URL url)?方法,設(shè)置包所在的?Archive?對(duì)應(yīng)的?url。
-
<2>?處,調(diào)用父類的?#loadClass(String name, boolean resolve)?方法,加載對(duì)應(yīng)的類。
如此,我們就實(shí)現(xiàn)了通過 LaunchedURLClassLoader 加載?jar?包中內(nèi)嵌的類。
666. 彩蛋
總體來說,Spring Boot?jar?啟動(dòng)的原理是非常清晰的,整體如下圖所示:
Spring Boot?jar?啟動(dòng)原理紅色部分,解決?jar?包中的類加載問題:
-
通過 Archive,實(shí)現(xiàn)?jar?包的遍歷,將?META-INF/classes?目錄和?META-INF/lib?的每一個(gè)內(nèi)嵌的?jar?解析成一個(gè) Archive 對(duì)象。
-
通過 Handler,處理?jar:?協(xié)議的 URL 的資源讀取,也就是讀取了每個(gè) Archive 里的內(nèi)容。
-
通過 LaunchedURLClassLoader,實(shí)現(xiàn)?META-INF/classes?目錄下的類和?META-INF/classes?目錄下內(nèi)嵌的?jar?包中的類的加載。具體的 URL 來源,是通過 Archive 提供;具體 URL 的讀取,是通過 Handler 提供。
橘色部分,解決 Spring Boot 應(yīng)用的啟動(dòng)問題:
-
通過 MainMethodRunner ,實(shí)現(xiàn) Spring Boot 應(yīng)用的啟動(dòng)類的執(zhí)行。
當(dāng)然,上述的一切都是通過 Launcher 來完成引導(dǎo)和啟動(dòng),通過?MANIFEST.MF?進(jìn)行具體配置。
😈 生活如此美好,本文就此結(jié)束!
另外,本文有兩個(gè)部分,胖友可以自己再去擼一擼,玩一玩:
-
WarLauncher 類:實(shí)現(xiàn) Spring Boot?war?包的啟動(dòng)。
-
org.springframework.boot.loader.jar?包:具體讀取?jar?的邏輯。
總結(jié)
以上是生活随笔為你收集整理的硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: String 字符串最长可以有多长?
- 下一篇: 消息队列面试连环炮,你抗得住吗?