SpringBoot應(yīng)用jar包啟動原理詳解
1、maven打包
Spring Boot項(xiàng)目的pom.xml文件中默認(rèn)使用spring-boot-maven-plugin
插件進(jìn)行打包:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
在執(zhí)行完maven clean package之后,會生成來個jar相關(guān)文件:
- test-0.0.1-SNAPSHOT.jar
- test-0.0.1-SNAPSHOT.jar.original
2、Jar包目錄結(jié)構(gòu)
以筆者的test-0.0.1-SNAPSHOT.jar
為例,來看一下jar的目錄結(jié)構(gòu),其中都包含哪些目錄和文件?
可以概述為:
spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 應(yīng)用程序
│ └── lib
│ └── 第三方依賴jar
└── org
└── springframework
└── boot
└── loader
└── springboot啟動程序
其中主要包括三大目錄:META-INF、BOOT-INF、org。
1)META-INF內(nèi)容
META-INF記錄了相關(guān)jar包的基礎(chǔ)信息,包括:入口程序。具體內(nèi)容如下:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: tms-start
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.saint.StartApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
- Main-Class是
org.springframework.boot.loader.JarLauncher
,即jar啟動的Main函數(shù); - Start-Class是
com.saint.StartApplication
,即我們自己SpringBoot項(xiàng)目的啟動類;也是下文提到的項(xiàng)目的引導(dǎo)類。
2)BOOT-INF內(nèi)容
- BOOT-INF/classes目錄:存放應(yīng)用編譯后的class文件源碼;
- BOOT-INF/lib目錄:存放應(yīng)用依賴的所有三方j(luò)ar包文件;
3)org內(nèi)容
org目錄下存放著所有SpringBoot相關(guān)的class文件,比如:JarLauncher、LaunchedURLClassLoader。
3、可執(zhí)行Jar(JarLauncher)
從jar包內(nèi)META-INF/MANIFEST.MF文件中的Main-Class
屬性值為org.springframework.boot.loader.JarLauncher
,可以看出main函數(shù)是JarLauncher,即:SpringBoot應(yīng)用中的Main-class屬性指向的class為org.springframework.boot.loader.JarLauncher
。
其實(shí)吧,主要是 Java官方文檔規(guī)定:java -jar命令引導(dǎo)的具體啟動類必須配置在MANIFEST.MF資源的Main-class屬性中;又根據(jù)“JAR文件規(guī)范”,MANIFEST.MF資源必須存放在/META-INF/目錄下。所以main函數(shù)才是JarLauncher
。
JarLauncher類繼承圖如下:
從JarLauncher
的類注釋我們看出JarLauncher的作用:
- 加載內(nèi)部/BOOT-INF/lib下的所有三方依賴jar;
- 加載內(nèi)部/BOOT-INF/classes下的所有應(yīng)用class;
1)JarLauncher的運(yùn)行步驟?
- 在解壓jar包后的根目錄下運(yùn)行 java org.springframework.boot.loader.JarLauncher。項(xiàng)目引導(dǎo)類(
META-INF/MANIFEST.MF
文件中的Start-Class
屬性)被JarLauncher加載并執(zhí)行。 - 如果直接運(yùn)行
Start-Class
(示例的StartApplication)類,會報(bào)錯ClassNotFoundException。 - Spring Boot依賴的JAR文件均存放在BOOT-INF/lib目錄下。
JarLauncher
會將這些JAR文件作為Start-Class
的類庫依賴。
這也是為什么JarLauncher能夠引導(dǎo),而直接運(yùn)行Start-Class
卻不行。
2)JarLauncher實(shí)現(xià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); } @Override protected 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); } }
JarLauncher#main()
中新建了JarLauncher
并調(diào)用父類Launcher中的launch()方法啟動程序;
- BOOT_INF_CLASSES、BOOT_INF_LIB變量對應(yīng)BOOT-INF/classes和lib路徑;
isNestedArchive(Archinve.Entry entry)
方法用于判斷FAT JAR資源的相對路徑是否為nestedArchive嵌套文檔。進(jìn)而決定這些FAT JAR是否會被launch。 當(dāng)方法返回false時(shí),說明FAT JAR被解壓至文件目錄。
1> Archive的概念
archive即歸檔文件,這個概念在linux下比較常見;通常就是一個tar/zip格式的壓縮包;而jar正是zip格式的。
SpringBoot抽象了Archive的概念,一個Archive可以是jar(JarFileArchive),也可以是文件目錄(ExplodedArchive);這樣也就統(tǒng)一了訪問資源的邏輯層;
public interface Archive extends Iterable<Archive.Entry>, AutoCloseable { .... }
Archive
繼承自Archive.Entry,Archive.Entry有兩種實(shí)現(xiàn):
JarFileArchive.JarFileEntry --> 基于java.util.jar.JarEntry實(shí)現(xiàn),表示FAT JAR嵌入資源。
ExplodedArchive.FileEntry --> 基于文件系統(tǒng)實(shí)現(xiàn);
兩者的主要差別是ExplodedArchive
相比于JarFileArchive
多了一個獲取文件的getFile()方法;
public File getFile() { return this.file; }
也就是說一個在jar包環(huán)境下尋找資源,一個在文件夾目錄下尋找資源;
所以從實(shí)現(xiàn)層面證明了JarLauncher支持JAR和文件系統(tǒng)兩種啟動方式。
當(dāng)執(zhí)行java -jar命令時(shí),將調(diào)用/META-INF /MANIFEST.MF文件的Main-Class屬性的main()方法,實(shí)際上調(diào)用的是JarLauncher#launch(args)方法;
3) Launcher#launch(args)方法
protected void launch(String[] args) throws Exception { if (!isExploded()) { // phase1:注冊jar URL處理器 JarFile.registerUrlProtocolHandler(); } // phase2:創(chuàng)建ClassLoader ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); String jarMode = System.getProperty("jarmode"); String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); // phase3:調(diào)用實(shí)際的引導(dǎo)類launch launch(args, launchClass, classLoader); }
launch()
方法分三步:
- 注冊jar URL處理器;
- 為所有的Archive創(chuàng)建可以加載jar in jar目錄的ClassLoader;
- 調(diào)用實(shí)際的引導(dǎo)類(Start-Class);
1> phase1 注冊jar URL處理器
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; public static void registerUrlProtocolHandler() { String handlers = System.getProperty(PROTOCOL_HANDLER, ""); System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); // 重置緩存的UrlHandlers; resetCachedUrlHandlers(); } private static void resetCachedUrlHandlers() { try { // 由URL類實(shí)現(xiàn):通過URL.setURLStreamHandlerFactory()獲得URLStreamHandler。 URL.setURLStreamHandlerFactory(null); } catch (Error ex) { // Ignore } }
JarFile#resetCachedUrlHandlers()
方法利用java.net.URLStreamHandler擴(kuò)展機(jī)制,實(shí)現(xiàn)由URL#getURLStreamHandler(String)
提供。
URL#getURLStreamHandler(String protocol)方法:
首先,URL的關(guān)聯(lián)協(xié)議(Protocol)對應(yīng)一種URLStreamHandler實(shí)現(xiàn)類。
JDK內(nèi)建了一些協(xié)議的實(shí)現(xiàn),這些實(shí)現(xiàn)均存放在sun.net.www.protocol
包下,并且類名必須為Handler,其類全名模式為sun.net.www.protocol.${protocol}.Handler
(包名前綴.協(xié)議名.Handler),其中${protocol}表示協(xié)議名
。
如果需要擴(kuò)展,則必須繼承URLStreamHandler類,通過配置Java系統(tǒng)屬性java.protocol.handler.pkgs
,追加URLStreamHandler實(shí)現(xiàn)類的package,多個package以“|”分割。
所以對于SpringBoot的JarFile,registerURLProtocolHandler()
方法將package org.springframework.boot.loader
追加到j(luò)ava系統(tǒng)屬性java.protocol.handler.pkgs
中。
也就是說,org.springframework.boot.loader包下存在協(xié)議對應(yīng)的Handler類,即org.springframework.boot.loader.jar.Handler;并且按照類名模式,其實(shí)現(xiàn)協(xié)議為JAR。
另外:在URL#getURLStreamHandler()
方法中,處理器先讀取Java系統(tǒng)屬性java.protocol.handler.pkgs
,無論其是否存在,繼續(xù)讀取sun.net.www.protocol
包;所以JDK內(nèi)建URLStreamHandler
實(shí)現(xiàn)是兜底的。
為什么SpringBoot要選擇覆蓋URLStreamHandler?
- Spring BOOT FAT JAR除包含傳統(tǒng)Java Jar資源之外,還包含依賴的JAR文件;即存在jar in jar的情況;
- 默認(rèn)情況下,JDK提供的ClassLoader只能識別jar中的class文件以及加載classpath下的其他jar包中的class文件,對于jar in jar的包無法加載;
- 當(dāng)SpringBoot FAT JAR被java -jar命令引導(dǎo)時(shí),其內(nèi)部的JAR文件無法被內(nèi)嵌實(shí)現(xiàn)sun.net.www.protocol.jar.Handler當(dāng)做class Path,故需要定義了一套URLStreamHandler實(shí)現(xiàn)類和JarURLConnection實(shí)現(xiàn)類,用來加載jar in jar包的class類文件;
2> phase2 創(chuàng)建可以加載jar in jar目錄的ClassLoader
獲取所有的Archive,然后針對每個Archive分別創(chuàng)建ClassLoader;
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); /** * 獲取所有的Archive(包含jar in jar的情況) */ protected Iterator<Archive> getClassPathArchivesIterator() throws Exception { return getClassPathArchives().iterator(); } /** * 針對每個Archive分別創(chuàng)建ClassLoader */ protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { List<URL> urls = new ArrayList<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return createClassLoader(urls.toArray(new URL[0])); }
3> phase3 調(diào)用實(shí)際的引導(dǎo)類(Start-Class)
// case1: 通過ExecutableArchiveLauncher#getMainClass()獲取MainClass String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); // 2、運(yùn)行實(shí)際的引導(dǎo)類 launch(args, launchClass, classLoader);
對于phase3,大致可以分為兩步:
- 首先通過
ExecutableArchiveLauncher#getMainClass()
獲取mainClass(即:/META-INF/MANIFEST.MF
資源中的Start-Class
屬性); - 利用反射獲取
mainClass
類中的main(Stirng[])方法并調(diào)用;
<1> 獲取mainClass:
Start-Class
屬性來自/META_INF/MANIFEST.MF
資源中。Launcher
的子類JarLauncher
或WarLauncher
沒有實(shí)現(xiàn)getMainClass()
方法。所以無論是Jar還是War,讀取的SpringBoot啟動類均來自此屬性。
<2> 執(zhí)行mainClass的main()方法:
獲取mainClass之后,MainMethodRunner#run()
方法利用反射獲取mainClass類中的main(Stirng[])方法并調(diào)用。
運(yùn)行JarLauncher實(shí)際上是在同進(jìn)程、同線程內(nèi)調(diào)用Start-Class類的main(Stirng[])方法,并且在調(diào)用前準(zhǔn)備好Class Path。
4、WarLauncher
WarLauncher是可執(zhí)行WAR的啟動器。
WarLauncher與JarLauncher的差異很小,主要區(qū)別在于項(xiàng)目文件和JAR Class Path路徑的不同。
- 相比于FAT Jar的目錄,WAR增加了WEB-INF/lib-provided,并且該目錄僅存放
<scope>provided</scope>
的JAR文件。 - 傳統(tǒng)的Servlet應(yīng)用的Class Path路徑僅關(guān)注WEB-INF/classes/和WEB-INF/lib/目錄,因此WEB-INF/lib-provided/中的JAR將被Servlet忽略。
好處:打包后的WAR文件能夠在Servlet容器中兼容運(yùn)行。
所以JarLauncher和WarLauncher并無本質(zhì)區(qū)別。
5、總結(jié)
Spring Boot應(yīng)用Jar/War的啟動流程:
Spring Boot應(yīng)用打包之后,生成一個Fat jar,包含了應(yīng)用依賴的所有三方j(luò)ar包和SpringBoot Loader相關(guān)的類。
Fat jar的啟動Main函數(shù)是JarLauncher,它負(fù)責(zé)創(chuàng)建一個LaunchedURLClassLoader來加載BOOT-INF/classes目錄以及/BOOT-INF/lib
下面的jar,并利用反射獲取mainClass
類中的main(Stirng[])方法并調(diào)用。即:運(yùn)行JarLauncher實(shí)際上是在同進(jìn)程、同線程內(nèi)調(diào)用Start-Class類的main(Stirng[])方法,并且在調(diào)用前準(zhǔn)備好Class Path。
其他點(diǎn):
SpringBoot通過擴(kuò)展JarFile、JarURLConnection及URLStreamHandler,實(shí)現(xiàn)了jar in jar中資源的加載。
SpringBoot通過擴(kuò)展URLClassLoader --> LauncherURLClassLoader,實(shí)現(xiàn)了jar in jar中class文件的加載。
WarLauncher相比JarLauncher只是多加載WEB-INF/lib-provided
目錄下的jar文件。
到此這篇關(guān)于SpringBoot應(yīng)用jar包啟動原理詳解的文章就介紹到這了,更多相關(guān)SpringBoot jar包啟動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Spring源碼深度解析(AOP功能源碼解析)
這篇文章主要介紹了關(guān)于Spring源碼深度解析(AOP功能源碼解析),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07SpringBoot?整合Security權(quán)限控制的初步配置
這篇文章主要為大家介紹了SpringBoot?整合Security權(quán)限控制的初步配置實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11SpringBoot 普通類調(diào)用Bean對象的一種方式推薦
這篇文章主要介紹了SpringBoot 普通類調(diào)用Bean對象的一種方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11mybaits-plus?lambdaQuery()?和?lambdaUpdate()?常見的使用方法
MyBatis-Plus是一個?MyBatis?(opens?new?window)的增強(qiáng)工具,在?MyBatis?的基礎(chǔ)上只做增強(qiáng)不做改變,為簡化開發(fā)、提高效率而生,這篇文章主要介紹了mybaits-plus?lambdaQuery()?和?lambdaUpdate()?比較常見的使用方法,需要的朋友可以參考下2023-01-01