title: Jar 包那些事
date: 2020/11/23 15:55
引言
公司中有一個(gè)系統(tǒng)用的是 Dubbo + SpringBoot 的架構(gòu),但后來(lái)發(fā)現(xiàn)好像并沒(méi)有必要用 dubbo 的架構(gòu)所以準(zhǔn)備直接讓 web 層引用 service 層的 jar 包,有如下問(wèn)題:
- Spring Boot 項(xiàng)目打包成的 jar ,被其他項(xiàng)目依賴之后,總是報(bào)找不到類的錯(cuò)誤腻要?
- 不使用
spring-boot-maven-plugin
插件打 jar 包,那么打進(jìn)去的配置文件會(huì)不會(huì)生效? -> 測(cè)試后發(fā)現(xiàn)生效的霞揉。
可執(zhí)行 jar 包 & 依賴 jar 包
一般使用 maven clean package
打包出來(lái)的 jar 包就是依賴 jar,引入一些插件我們就可以將其打成可執(zhí)行 jar晰骑,一般而言兩者的目錄結(jié)構(gòu)沒(méi)有什么區(qū)別(見(jiàn)附1)适秩,唯一有區(qū)別的是 MANIFEST.MF(清單文件)。
依賴 jar 包的 MANIFEST.MF
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: x5456
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_181
可執(zhí)行 jar 包的 MANIFEST.MF
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: x5456
Class-Path: lib/hutool-all-4.6.1.jar # 依賴 jar 包的路徑
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_181
Main-Class: cn.x5456.TestCanRunJar # 使用命令(java -jar xxx.jar)運(yùn)行時(shí)的主啟動(dòng)類
META-INF 文件夾是干啥的硕舆,META-INF文件夾能刪嗎秽荞?
如果你將 Jar 中的 META-INF 文件夾刪除,那么 jar 文件里邊就沒(méi)有 MANIFEST.MF 文件抚官。那么扬跋,java -jar 就找不到 main class.
沒(méi)有 META-INF 你仍然可以創(chuàng)建一個(gè) Jar 文件。但是凌节,當(dāng)你想要執(zhí)行 jar 文件的時(shí)候钦听,這個(gè) jar 是需要具備 META-INF/MANIFEST.MF 的洒试。
附1:可執(zhí)行 jar 與依賴 jar 打包出來(lái)的文件結(jié)構(gòu)
SpringBoot 打包的奧秘
這個(gè)是我們對(duì)一個(gè) SpringBoot 項(xiàng)目執(zhí)行 maven clean package
命令打包出來(lái)的結(jié)構(gòu):
我們發(fā)現(xiàn)這里有兩個(gè)文件,第一個(gè) spring-test-0.0.1-SNAPSHOT.jar 表示打包成的可執(zhí)行 jar 朴上,第二個(gè) spring-test-0.0.1-SNAPSHOT.jar.original 則是在打包過(guò)程中 垒棋,被重命名的 jar,這是一個(gè)不可執(zhí)行 jar痪宰,但是可以被其他項(xiàng)目依賴的 jar叼架。
original:原件
為什么會(huì)有兩個(gè)文件呢?
在新建 SpringBoot 項(xiàng)目的時(shí)候衣撬,我們引入了一個(gè)插件:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
spring-boot-maven-plugin 項(xiàng)目存在于 spring-boot-tools 目錄中碉碉,它的默認(rèn)目標(biāo)(goal)就是 repackage 功能,其他功能要使用淮韭,則需要開(kāi)發(fā)者顯式配置垢粮。
spring-boot-maven-plugin 的 repackage 能夠?qū)?mvn package 生成的軟件包,再次打包為可執(zhí)行的軟件包靠粪,并將 mvn package 生成的軟件包重命名為 *.original蜡吧。
spring-boot-maven-plugin 的 repackage 在代碼層面調(diào)用了 RepackageMojo的execute 方法,而在該方法中又調(diào)用了repackage 方法占键,repackage 方法代碼及操作解析如下:
private void repackage() throws MojoExecutionException {
// maven生成的jar昔善,最終的命名將加上.original后綴
Artifact source = getSourceArtifact();
// 最終為可執(zhí)行jar,即fat jar
File target = getTargetFile();
// 獲取重新打包器畔乙,將maven生成的jar重新打包成可執(zhí)行jar
Repackager repackager = getRepackager(source.getFile());
// 查找并過(guò)濾項(xiàng)目運(yùn)行時(shí)依賴的jar
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
// 將artifacts轉(zhuǎn)換成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
try {
// 獲得Spring Boot啟動(dòng)腳本
LaunchScript launchScript = getLaunchScript();
// 執(zhí)行重新打包君仆,生成fat jar
repackager.repackage(target, libraries, launchScript);
} catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
// 將maven生成的jar更新成.original文件
updateArtifact(source, target, repackager.getBackupFile());
}
兩個(gè) jar 包比較
可執(zhí)行 jar spring-test-0.0.1-SNAPSHOT.jar
解壓之后,目錄結(jié)構(gòu)如下:
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── cn
│ │ └── x5456 # 應(yīng)用的.class 文件目錄
│ │ └── springtest
│ │ └── SpringTestApplication.class
│ └── lib # 這里存放的是應(yīng)用的 Maven 依賴的jar包文件
│ ├── javax.annotation-api-1.3.2.jar
│ ├── spring-beans-5.1.8.RELEASE.jar
│ └── ...
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.x5456
│ └── springtest
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader #存放的是 Spring boot loader 的 class 文件
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│ ├── Archive$Entry.class
│ ├── ...
├── data
│ ├── RandomAccessData.class
│ ├── ...
├── jar
│ ├── AsciiBytes.class
│ ├── ...
└── util
└── SystemPropertyUtils.class
我們的代碼被放在了 BOOT-INF/classes/
目錄下牲距。
META-INF
目錄存放著當(dāng)前 jar 包的清單文件(MANIFEST.MF)和當(dāng)前 jar 包引入的 maven 依賴信息(pom.xml)返咱,我們看下 MANIFEST.MF 文件:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: spring-test
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: cn.x5456.springtest.SpringTestApplication # 這個(gè)是 Spring 自定義的屬性,存放我們的啟動(dòng)類的全類路徑
Spring-Boot-Classes: BOOT-INF/classes/ # 這個(gè)是 Spring 自定義的屬性牍鞠,表示我們自己代碼編譯后的位置
Spring-Boot-Lib: BOOT-INF/lib/ # 這個(gè)是 Spring 自定義的屬性咖摹,表示項(xiàng)目依賴的 jar 的位置
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.2.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher # 當(dāng)使用 java -jar xxx.jar 命令啟動(dòng)的時(shí)候會(huì)調(diào)用這個(gè)類的 main 方法。
依賴 jar spring-test-0.0.1-SNAPSHOT.jar.original
解壓之后难述,目錄結(jié)構(gòu)如下:
依賴 jar 并沒(méi)有 BOOT-INF/classes
目錄萤晴,而且也沒(méi)有把它所依賴的 jar 包打進(jìn)來(lái),當(dāng)其他項(xiàng)目引用這個(gè) jar 包的時(shí)候(通過(guò) maven 坐標(biāo))胁后,他會(huì)去找 META-INF/maven/pom.xml
文件店读,將當(dāng)前 jar 包所依賴的其他 jar 包從 maven 倉(cāng)庫(kù)拉下來(lái)。
我們順道看下它的 META-INF/MANIFEST.MF
文件攀芯,其中沒(méi)有定義啟動(dòng)類等屯断。
Manifest-Version: 1.0
Implementation-Title: spring-test
Implementation-Version: 0.0.1-SNAPSHOT
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0
為什么 SpringBoot 不直接把我們的代碼放到根路徑,而要自定義一個(gè) BOOT-INF 目錄呢?
其他的可執(zhí)行 jar 都是直接把他放在了打包的類路徑下裹纳,這樣就可以做到既可以執(zhí)行又可以引用择葡,那么 SpringBoot 為什么這樣做呢,他的奧秘就在于 MANIFEST.MF 文件中的 Main-Class 配置的并不是我們的主啟動(dòng)類剃氧,而是 JarLauncher 這個(gè)類敏储,沒(méi)有放在根路徑的原因可能是害怕我們創(chuàng)建了一個(gè)相同類路徑的類將其覆蓋。
Main-Class: org.springframework.boot.loader.JarLauncher # 當(dāng)使用 java -jar xxx.jar 命令啟動(dòng)的時(shí)候會(huì)調(diào)用這個(gè)類的 main 方法朋鞍。
那么 JarLauncher 是做什么的呢已添?
其他項(xiàng)目引用 SpringBoot 打成的 jar 包
我們對(duì) spring-test 這個(gè)項(xiàng)目執(zhí)行 maven clean install
命令滥酥,將其打包到 maven 倉(cāng)庫(kù)更舞。
其他項(xiàng)目,通過(guò) maven 引用這個(gè) jar 包坎吻,調(diào)用它里面的 JsonUtils 工具類:
我們發(fā)現(xiàn)根本就調(diào)用不了這個(gè)方法(因?yàn)?SpringBoot 打包出來(lái)的 jar 包結(jié)構(gòu)和正常的不一樣缆蝉,他把我們的代碼放到了 BOOT-INF 目錄下了)。
解決:一次打包兩個(gè) jar
一般來(lái)說(shuō)瘦真,Spring Boot 直接打包成可執(zhí)行 jar 就可以了刊头,不建議將 Spring Boot 作為普通的 jar 被其他的項(xiàng)目所依賴。如果有這種需求诸尽,建議將被依賴的部分原杂,單獨(dú)抽出來(lái)做一個(gè)普通的 Maven 項(xiàng)目,然后在 Spring Boot 中引用這個(gè) Maven 項(xiàng)目您机。
如果非要將 Spring Boot 打包成一個(gè)普通 jar 被其他項(xiàng)目依賴穿肄,技術(shù)上來(lái)說(shuō),也是可以的际看,給 spring-boot-maven-plugin 插件添加如下配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
配置的 classifier 表示可執(zhí)行 jar 的名字咸产,配置了這個(gè)之后,在插件執(zhí)行 repackage 命令時(shí)仿村,就不會(huì)給 mvn package
所打成的 jar 重命名了锐朴,所以,打包后的 jar 如下:
我們執(zhí)行 maven clean install
將其打包到倉(cāng)庫(kù):
這個(gè)時(shí)候其他項(xiàng)目引用他就可以調(diào)用它里面的方法了:
為啥 MAIN-CLASS 是 XXXLauncher?
XXXLauncher 在調(diào)用我們自己的主類之前做了以下三件事:
- 擴(kuò)展JDK默認(rèn)的支持JAR對(duì)應(yīng)的協(xié)議蔼囊,因?yàn)镾pring Boot啟動(dòng)不僅僅需要JDK的JAR文件,還需要BOOT-INF/lib這個(gè)目錄下的文件衣迷。默認(rèn)實(shí)現(xiàn)無(wú)法將BOOT-INF/lib這個(gè)目錄當(dāng)作ClassPath畏鼓,故需要替換實(shí)現(xiàn)。
- 判斷當(dāng)前的介質(zhì)壶谒,是java -jar啟動(dòng)云矫,還是java org.springframework.boot.loader.JarLauncher啟動(dòng)。以便獲取對(duì)應(yīng)的ClassLoader汗菜。
- 獲取MANIFEST.MF文件中的Start-Class屬性让禀,也就是我們自定義的主類挑社。通過(guò)第二步獲取的ClassLoader加載獲取到Class文件,通過(guò)反射調(diào)用main方法巡揍,啟動(dòng)應(yīng)用痛阻。
項(xiàng)目打成 war 包的結(jié)構(gòu)
我們發(fā)現(xiàn)他打出來(lái)的結(jié)構(gòu)中多了一個(gè) org 目錄,通過(guò)上面的學(xué)習(xí)我們知道這個(gè)目錄里放的是啟動(dòng)器(Launcher)相關(guān)的類腮敌,而啟動(dòng)器又是在 MANIFEST.MF 文件中配置的阱当,所以我們看下:
Manifest-Version: 1.0
Implementation-Title: spring-test
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: cn.x5456.springtest.SpringTestApplication
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.2.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.WarLauncher # 與打成 jar 包的相比,這里換成了 WarLauncher
Spring 官方解釋這樣做的目的是:打包一個(gè)又能發(fā)布于 tomcat 又能 java -jar 直接跑的war糜工。
JarLauncher 與 WarLauncher 區(qū)別
差別僅在于弊添,JarLauncher在構(gòu)建LauncherURLClassLoader時(shí),會(huì)搜索BOOT-INF/classes目錄及BOOT-INF/lib目錄下jar捌木,WarLauncher在構(gòu)建LauncherURLClassLoader時(shí)油坝,則會(huì)搜索WEB-INFO/classes目錄及WEB-INFO/lib和WEB-INFO/lib-provided兩個(gè)目錄下的jar
參考文章
Spring Boot 打包成的可執(zhí)行 jar ,為什么不能被其他項(xiàng)目依賴刨裆?
下面的這篇文章,等學(xué)懂了 JVM 類加載機(jī)制再回來(lái)看
springboot應(yīng)用啟動(dòng)原理(二) 擴(kuò)展URLClassLoader實(shí)現(xiàn)嵌套jar加載