Jar 包那些事


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)題:

  1. Spring Boot 項(xiàng)目打包成的 jar ,被其他項(xiàng)目依賴之后,總是報(bào)找不到類的錯(cuò)誤腻要?
  2. 不使用 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)

image

SpringBoot 打包的奧秘

這個(gè)是我們對(duì)一個(gè) SpringBoot 項(xiàng)目執(zhí)行 maven clean package 命令打包出來(lái)的結(jié)構(gòu):

image

我們發(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>
image

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)如下:

image

依賴 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 是做什么的呢已添?

Spring-Boot啟動(dòng)之前做了哪些事?

其他項(xiàng)目引用 SpringBoot 打成的 jar 包

我們對(duì) spring-test 這個(gè)項(xiàng)目執(zhí)行 maven clean install 命令滥酥,將其打包到 maven 倉(cāng)庫(kù)更舞。

image
image

其他項(xiàng)目,通過(guò) maven 引用這個(gè) jar 包坎吻,調(diào)用它里面的 JsonUtils 工具類:

image

我們發(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 如下:

image

我們執(zhí)行 maven clean install 將其打包到倉(cāng)庫(kù):

image

這個(gè)時(shí)候其他項(xiàng)目引用他就可以調(diào)用它里面的方法了:

image

為啥 MAIN-CLASS 是 XXXLauncher?

XXXLauncher 在調(diào)用我們自己的主類之前做了以下三件事:

  1. 擴(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)。
  2. 判斷當(dāng)前的介質(zhì)壶谒,是java -jar啟動(dòng)云矫,還是java org.springframework.boot.loader.JarLauncher啟動(dòng)。以便獲取對(duì)應(yīng)的ClassLoader汗菜。
  3. 獲取MANIFEST.MF文件中的Start-Class屬性让禀,也就是我們自定義的主類挑社。通過(guò)第二步獲取的ClassLoader加載獲取到Class文件,通過(guò)反射調(diào)用main方法巡揍,啟動(dòng)應(yīng)用痛阻。

項(xiàng)目打成 war 包的結(jié)構(gòu)

image

我們發(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糜工。

image

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)目依賴刨裆?

聊一聊 JAR 文件和 MANIFEST.MF

Spring-Boot啟動(dòng)之前做了哪些事澈圈?

下面的這篇文章,等學(xué)懂了 JVM 類加載機(jī)制再回來(lái)看

springboot應(yīng)用啟動(dòng)原理(二) 擴(kuò)展URLClassLoader實(shí)現(xiàn)嵌套jar加載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崔拥,一起剝皮案震驚了整個(gè)濱河市极舔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌链瓦,老刑警劉巖拆魏,帶你破解...
    沈念sama閱讀 210,835評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異慈俯,居然都是意外死亡渤刃,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門(mén)贴膘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)卖子,“玉大人,你說(shuō)我怎么就攤上這事刑峡⊙竺觯” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,481評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵突梦,是天一觀的道長(zhǎng)诫舅。 經(jīng)常有香客問(wèn)我,道長(zhǎng)宫患,這世上最難降的妖魔是什么刊懈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,303評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上虚汛,老公的妹妹穿的比我還像新娘匾浪。我一直安慰自己,他們只是感情好卷哩,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,375評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布蛋辈。 她就那樣靜靜地躺著,像睡著了一般殉疼。 火紅的嫁衣襯著肌膚如雪梯浪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,729評(píng)論 1 289
  • 那天瓢娜,我揣著相機(jī)與錄音挂洛,去河邊找鬼。 笑死眠砾,一個(gè)胖子當(dāng)著我的面吹牛虏劲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播褒颈,決...
    沈念sama閱讀 38,877評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼柒巫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了谷丸?” 一聲冷哼從身側(cè)響起堡掏,我...
    開(kāi)封第一講書(shū)人閱讀 37,633評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刨疼,沒(méi)想到半個(gè)月后泉唁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,088評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡揩慕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,443評(píng)論 2 326
  • 正文 我和宋清朗相戀三年亭畜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迎卤。...
    茶點(diǎn)故事閱讀 38,563評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拴鸵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜗搔,到底是詐尸還是另有隱情劲藐,我是刑警寧澤,帶...
    沈念sama閱讀 34,251評(píng)論 4 328
  • 正文 年R本政府宣布樟凄,位于F島的核電站瘩燥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏不同。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,827評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望二拐。 院中可真熱鬧服鹅,春花似錦、人聲如沸百新。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,712評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)饭望。三九已至仗哨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铅辞,已是汗流浹背厌漂。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,943評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斟珊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,240評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像园担,于是被迫代替她去往敵國(guó)和親祝峻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,435評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容