Jar包沖突的本質(zhì)是什么?
Java應(yīng)用程序因某種因素,加載不到正確的類而導(dǎo)致其行為跟預(yù)期不一致。
具體來說可分為兩種情況:
- 應(yīng)用程序依賴的同一個(gè)Jar包出現(xiàn)了多個(gè)不同版本,并選擇了錯(cuò)誤的版本而導(dǎo)致JVM加載不到需要的類或加載了錯(cuò)誤版本的類坝辫,為了敘述的方便,暫且稱之為第一類Jar包沖突問題射亏;
- 同樣的類(類的全限定名完全一樣)出現(xiàn)在多個(gè)不同的依賴Jar包中近忙,即該類有多個(gè)版本,并由于Jar包加載的先后順序?qū)е翵VM加載了錯(cuò)誤版本的類智润,稱之為第二類Jar包問題及舍。這兩種情況所導(dǎo)致的結(jié)果其實(shí)是一樣的,都會(huì)使應(yīng)用程序加載不到正確的類窟绷,那其行為自然會(huì)跟預(yù)期不一致了锯玛,以下對(duì)這兩種類型進(jìn)行詳細(xì)分析。
同一個(gè)Jar包出現(xiàn)了多個(gè)不同版本
隨著Jar包迭代升級(jí),我們所依賴的開源的或公司內(nèi)部的Jar包工具都會(huì)存在若干不同的版本攘残,而版本升級(jí)自然就避免不了類的方法簽名變更拙友,甚至于類名的更替,而我們當(dāng)前的應(yīng)用程序往往依賴特定版本的某個(gè)類 M 歼郭,由于maven的傳遞依賴而導(dǎo)致同一個(gè)Jar包出現(xiàn)了多個(gè)版本遗契,當(dāng)maven的仲裁機(jī)制選擇了錯(cuò)誤的版本時(shí),而恰好類 M在該版本中被去掉了病曾,或者方法簽名改了牍蜂,導(dǎo)致應(yīng)用程序因找不到所需的類 M或找不到類 M中的特定方法,就會(huì)出現(xiàn)第一類Jar沖突問題知态。
可總結(jié)出該類沖突問題發(fā)生的以下三個(gè)必要條件:
- 由于maven的傳遞依賴導(dǎo)致依賴樹中出現(xiàn)了同一個(gè)Jar包的多個(gè)版本
- 該Jar包的多個(gè)版本之間存在接口差異捷兰,如類名更替立叛,方法簽名更替等负敏,且應(yīng)用程序依賴了其中有變更的類或方法
- maven的仲裁機(jī)制選擇了錯(cuò)誤的版本
同一個(gè)類出現(xiàn)在多個(gè)不同Jar包中
同樣的類出現(xiàn)在了應(yīng)用程序所依賴的兩個(gè)及以上的不同Jar包中,這會(huì)導(dǎo)致什么問題呢秘蛇?我們知道其做,同一個(gè)類加載器對(duì)于同一個(gè)類只會(huì)加載一次,那么當(dāng)一個(gè)類出現(xiàn)在了多個(gè)Jar包中赁还,假設(shè)有 A 妖泄、 B 、 C 等艘策,由于Jar包依賴的路徑長短蹈胡、聲明的先后順序或文件系統(tǒng)的文件加載順序等原因,類加載器首先從Jar包 A 中加載了該類后朋蔫,就不會(huì)加載其余Jar包中的這個(gè)類了罚渐,那么問題來了:如果應(yīng)用程序此時(shí)需要的是Jar包 B 中的類版本,并且該類在Jar包 A 和 B 中有差異(方法不同驯妄、成員不同等等)荷并,而JVM卻加載了Jar包 A 的中的類版本,與期望不一致青扔,自然就會(huì)出現(xiàn)各種詭異的問題源织。
從上面的描述中,可以發(fā)現(xiàn)出現(xiàn)不同Jar包的沖突問題有以下三個(gè)必要條件:
- 同一個(gè)類 M 出現(xiàn)在了多個(gè)依賴的Jar包中微猖,為了敘述方便谈息,假設(shè)還是兩個(gè): A 和 B
- Jar包 A 和 B 中的該類 M 有差異,無論是方法簽名不同也好凛剥,成員變量不同也好黎茎,只要可以造成實(shí)際加載的類的行為和期望不一致都行。如果說Jar包 A 和 B 中的該類完全一樣当悔,那么類加載器無論先加載哪個(gè)Jar包傅瞻,得到的都是同樣版本的類 M 踢代,不會(huì)有任何影響,也就不會(huì)出現(xiàn)Jar包沖突帶來的詭異問題嗅骄。
- 加載的類 M 不是所期望的版本胳挎,即加載了錯(cuò)誤的Jar包
沖突的產(chǎn)生原因
maven仲裁機(jī)制
當(dāng)前maven大行其道,說到第一類Jar包沖突問題的產(chǎn)生原因溺森,就不得不提maven的依賴機(jī)制傳遞性依賴是Maven2.0引入的新特性慕爬,讓我們只需關(guān)注直接依賴的Jar包,對(duì)于間接依賴的Jar包屏积,Maven會(huì)通過解析從遠(yuǎn)程倉庫獲取的依賴包的pom文件來隱式地將其引入医窿,這為我們開發(fā)帶來了極大的便利,但與此同時(shí)炊林,也帶來了常見的問題——版本沖突姥卢,即同一個(gè)Jar包出現(xiàn)了多個(gè)不同的版本,針對(duì)該問題Maven也有一套仲裁機(jī)制來決定最終選用哪個(gè)版本渣聚,但Maven的選擇往往不一定是我們所期望的独榴,這也是產(chǎn)生Jar包沖突最常見的原因之一。先來看下Maven的仲裁機(jī)制:
- 優(yōu)先按照依賴管理<dependencyManagement>元素中指定的版本聲明進(jìn)行仲裁奕枝,此時(shí)下面的兩個(gè)原則都無效了
- 若無版本聲明棺榔,則按照“短路徑優(yōu)先”的原則(Maven2.0)進(jìn)行仲裁,即選擇依賴樹中路徑最短的版本
- 若路徑長度一致隘道,則按照“第一聲明優(yōu)先”的原則進(jìn)行仲裁症歇,即選擇POM中最先聲明的版本
從maven的仲裁機(jī)制中可以發(fā)現(xiàn),除了第一條仲裁規(guī)則(這也是解決Jar包沖突的常用手段之一)外谭梗,后面的兩條原則忘晤,對(duì)于同一個(gè)Jar包不同版本的選擇,maven的選擇有點(diǎn)“一廂情愿”了默辨,也許這是maven研發(fā)團(tuán)隊(duì)在總結(jié)了大量的項(xiàng)目依賴管理經(jīng)驗(yàn)后得出的兩條結(jié)論德频,又或者是發(fā)現(xiàn)根本找不到一種統(tǒng)一的方式來滿足所有場景之后的無奈之舉,可能這對(duì)于多數(shù)場景是適用的缩幸,但是它不一定適合我——當(dāng)前的應(yīng)用壹置,因?yàn)槊總€(gè)應(yīng)用都有其特殊性,該依賴哪個(gè)版本表谊,maven沒辦法幫你完全搞定钞护,如果你沒有規(guī)規(guī)矩矩地使用<dependencyManagement>來進(jìn)行依賴管理,就注定了逃脫不了第一類Jar包沖突問題爆办。
例子
如果項(xiàng)目的依賴A和依賴B同時(shí)引入了依賴C难咕。
如果依賴C在A和B中的版本不一致就可能依賴沖突。
比如 項(xiàng)目 <- A, B, A <- C(1.0)余佃,B <- C(1.1)暮刃。
那么maven如果選擇高版本C(1.1)來導(dǎo)入(這個(gè)選擇maven會(huì)根據(jù)不等路徑短路徑原則和同等路徑第一聲明原則選取),C(1.0)中的類c在C(1.1)中被修改而不存在了爆土。
在編譯期可能并不會(huì)報(bào)錯(cuò)椭懊,因?yàn)榫幾g的目的只是把業(yè)務(wù)源代碼編譯成class文件,所以如果項(xiàng)目源代碼中沒有引入共有依賴C因升級(jí)而缺失的類c步势,就不會(huì)出現(xiàn)編譯失敗氧猬。除非源代碼就引入了共有依賴C因升級(jí)而缺失的類c則會(huì)直接編譯失敗。
在運(yùn)行期坏瘩,很有可能出現(xiàn)依賴A在執(zhí)行過程中調(diào)用C(1.0)以前有但是升級(jí)到C(1.1)就缺失的類c盅抚,導(dǎo)致運(yùn)行期失敗,出現(xiàn)很典型的依賴沖突時(shí)的NoClassDefFoundError錯(cuò)誤倔矾。
如果是升級(jí)后出現(xiàn)原有的方法被修改而不存在的情況時(shí)妄均,就會(huì)拋出NoSuchMethodError錯(cuò)誤
Jar包的加載順序
對(duì)于第二類Jar包沖突問題,即多個(gè)不同的Jar包有類沖突破讨,這相對(duì)于第一類問題就顯得更為棘手丛晦。為什么這么說呢奕纫?在這種情況下提陶,兩個(gè)不同的Jar包,假設(shè)為 A匹层、 B隙笆,它們的名稱互不相同,甚至可能完全不沾邊升筏,如果不是出現(xiàn)沖突問題撑柔,你可能都不會(huì)發(fā)現(xiàn)它們有共有的類!對(duì)于A您访、B這兩個(gè)Jar包铅忿,maven就顯得無能為力了,因?yàn)閙aven只會(huì)為你針對(duì)同一個(gè)Jar包的不同版本進(jìn)行仲裁灵汪,而這倆是屬于不同的Jar包檀训,超出了maven的依賴管理范疇。此時(shí)享言,當(dāng)A峻凫、B都出現(xiàn)在應(yīng)用程序的類路徑下時(shí),就會(huì)存在潛在的沖突風(fēng)險(xiǎn)览露,即A荧琼、B的加載先后順序就決定著JVM最終選擇的類版本,如果選錯(cuò)了,就會(huì)出現(xiàn)詭異的第二類沖突問題命锄。
那么Jar包的加載順序都由哪些因素決定的呢堰乔?具體如下:
- Jar包所處的加載路徑,或者換個(gè)說法就是加載該Jar包的類加載器在JVM類加載器樹結(jié)構(gòu)中所處層級(jí)脐恩。由于JVM類加載的雙親委派機(jī)制浩考,層級(jí)越高的類加載器越先加載其加載路徑下的類,顧名思義被盈,引導(dǎo)類加載器(bootstrap ClassLoader析孽,也叫啟動(dòng)類加載器)是最先加載其路徑下Jar包的,其次是擴(kuò)展類加載器(extension ClassLoader)只怎,再次是系統(tǒng)類加載器(system ClassLoader袜瞬,也就是應(yīng)用加載器appClassLoader),Jar包所處加載路徑的不同身堡,就決定了它的加載順序的不同邓尤。
- 文件系統(tǒng)的文件加載順序。這個(gè)因素很容易被忽略贴谎,而往往又是因環(huán)境不一致而導(dǎo)致各種詭異沖突問題的罪魁禍?zhǔn)坠R騮omcat、resin等容器的ClassLoader獲取加載路徑下的文件列表時(shí)是不排序的擅这,這就依賴于底層文件系統(tǒng)返回的順序澈魄,那么當(dāng)不同環(huán)境之間的文件系統(tǒng)不一致時(shí),就會(huì)出現(xiàn)有的環(huán)境沒問題仲翎,有的環(huán)境出現(xiàn)沖突痹扇。例如,對(duì)于Linux操作系統(tǒng)溯香,返回順序則是由iNode的順序來決定的鲫构,如果說測試環(huán)境的Linux系統(tǒng)與線上環(huán)境不一致時(shí),就極有可能出現(xiàn)典型案例:測試環(huán)境怎么測都沒問題玫坛,但一上線就出現(xiàn)沖突問題结笨,規(guī)避這種問題的最佳辦法就是盡量保證測試環(huán)境與線上一致。
沖突的表象
Jar包沖突可能會(huì)導(dǎo)致哪些問題湿镀?通常發(fā)生在編譯或運(yùn)行時(shí)炕吸,主要分為兩類問題:一類是比較直觀的也是最為常見的錯(cuò)誤是拋出各種運(yùn)行時(shí)異常,還有一類就是比較隱晦的問題肠骆,它不會(huì)報(bào)錯(cuò)算途,其表現(xiàn)形式是應(yīng)用程序的行為跟預(yù)期不一致,分條羅列如下:
java.lang.ClassNotFoundException蚀腿,即java類找不到嘴瓤。這類典型異常通常是由于扫外,沒有在依賴管理中聲明版本,maven的仲裁的時(shí)候選取了錯(cuò)誤的版本廓脆,而這個(gè)版本缺少我們需要的某個(gè)class而導(dǎo)致該錯(cuò)誤筛谚。例如httpclient-4.4.jar升級(jí)到httpclient-4.36.jar時(shí),類org.apache.http.conn.ssl.NoopHostnameVerifier被去掉了停忿,如果此時(shí)我們本來需要的是4.4版本驾讲,且用到了NoopHostnameVerifier這個(gè)類,而maven仲裁時(shí)選擇了4.6席赂,則會(huì)導(dǎo)致ClassNotFoundException異常吮铭。
java.lang.NoSuchMethodError,即找不到特定方法颅停,第一類沖突和第二類沖突都可能導(dǎo)致該問題——加載的類不正確谓晌。若是第一類沖突,則是由于錯(cuò)誤版本的Jar包與所需要版本的Jar包中的類接口不一致導(dǎo)致癞揉,例如antlr-2.7.2.jar升級(jí)到antlr-2.7.6.Jar時(shí)纸肉,接口antlr.collections.AST.getLine()發(fā)生變動(dòng),當(dāng)maven仲裁選擇了錯(cuò)誤版本而加載了錯(cuò)誤版本的類AST喊熟,則會(huì)導(dǎo)致該異常柏肪;若是第二類沖突,則是由于不同Jar包含有的同名類接口不一致導(dǎo)致芥牌,典型的案例:Apache的commons-lang包烦味,2.x升級(jí)到3.x時(shí),包名直接從commons-lang改為commons-lang3胳泉,部分接口也有所改動(dòng)拐叉,由于包名不同和傳遞性依賴岩遗,經(jīng)常會(huì)出現(xiàn)兩種Jar包同時(shí)在classpath下扇商,org.apache.commons.lang.StringUtils.isBlank就是其中有差異的接口之一,由于Jar包的加載順序宿礁,導(dǎo)致加載了錯(cuò)誤版本的StringUtils類案铺,就可能出現(xiàn)NoSuchMethodError異常。
java.lang.NoClassDefFoundError梆靖,java.lang.LinkageError等控汉,原因和上述雷同,
沒有報(bào)錯(cuò)異常返吻,但應(yīng)用的行為跟預(yù)期不一致姑子。這類問題同樣也是由于運(yùn)行時(shí)加載了錯(cuò)誤版本的類導(dǎo)致,但跟前面不同的是测僵,沖突的類接口都是一致的街佑,但具體實(shí)現(xiàn)邏輯有差異谢翎,當(dāng)我們加載的類版本不是我們需要的實(shí)現(xiàn)邏輯,就會(huì)出現(xiàn)行為跟預(yù)期不一致問題沐旨。這類問題通常發(fā)生在我們自己內(nèi)部實(shí)現(xiàn)的多個(gè)Jar包中森逮,由于包路徑和類名命名不規(guī)范等問題,導(dǎo)致兩個(gè)不同的Jar包出現(xiàn)了接口一致但實(shí)現(xiàn)邏輯又各不相同的同名類磁携,從而引發(fā)此問題褒侧。
解決方案
- 如果有異常堆棧信息,根據(jù)錯(cuò)誤信息即可定位導(dǎo)致沖突的類名
- 若步驟1無法定位沖突的類來自哪個(gè)Jar包谊迄,可在應(yīng)用程序啟動(dòng)時(shí)加上JVM參數(shù)-verbose:class或者-XX:+TraceClassLoading闷供,日志里會(huì)打印出每個(gè)類的加載信息,如來自哪個(gè)Jar包
- 定位了沖突類的Jar包之后统诺,通過mvn dependency:tree -Dverbose -Dincludes=<groupId>:<artifactId>查看是哪些地方引入的Jar包的這個(gè)版本
- 確定Jar包來源之后这吻,如果是第一類Jar包沖突,則可用<excludes>排除不需要的Jar包版本或者在依賴管理<dependencyManagement>中申明版本篙议;若是第二類Jar包沖突唾糯,如果可排除,則用<excludes>排掉不需要的那個(gè)Jar包鬼贱,若不能排移怯,則需考慮Jar包的升級(jí)或換個(gè)別的Jar包。
沖突檢測插件
對(duì)于第二類Jar包沖突問題这难,前面也提到過舟误,其核心在于同名類出現(xiàn)在了多個(gè)不同的Jar包中,如果人工來排查該問題姻乓,則需要逐個(gè)點(diǎn)開每個(gè)Jar包嵌溢,然后相互對(duì)比看有沒同名的類夸政,那得多么浪費(fèi)精力袄焯摺?乡翅!好在這種費(fèi)時(shí)費(fèi)力的體力活能交給程序去干剪个。maven-enforcer-plugin秧骑,這個(gè)強(qiáng)大的maven插件,配合extra-enforcer-rules工具扣囊,能自動(dòng)掃描Jar包將沖突檢測并打印出來乎折,其原理其實(shí)也比較簡單,通過掃描Jar包中的class侵歇,記錄每個(gè)class對(duì)應(yīng)的Jar包列表骂澄,如果有多個(gè)即是沖突了,故不必深究惕虑,我們只需要關(guān)注如何用它即可坟冲。
在最終需要打包運(yùn)行的應(yīng)用模塊pom中士修,引入maven-enforcer-plugin的依賴,在build階段即可發(fā)現(xiàn)問題樱衷,并解決它棋嘲。比如對(duì)于具有parent pom的多模塊項(xiàng)目,需要將插件依賴聲明在應(yīng)用模塊的pom中矩桂。這里有童鞋可能會(huì)疑問沸移,為什么不把插件依賴聲明在parent pom中呢?那樣依賴它的應(yīng)用子模塊豈不是都能復(fù)用了侄榴?這里之所以強(qiáng)調(diào)“打包運(yùn)行的應(yīng)用模塊pom”雹锣,是因?yàn)闆_突檢測針對(duì)的是最終集成的應(yīng)用,關(guān)注的是應(yīng)用運(yùn)行時(shí)是否會(huì)出現(xiàn)沖突問題癞蚕,而每個(gè)不同的應(yīng)用模塊蕊爵,各自依賴的Jar包集合是不同的,由此而產(chǎn)生的<ignoreClasses>列表也是有差異的桦山,因此只能針對(duì)應(yīng)用模塊pom分別引入該插件攒射。
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
<rules>
<dependencyConvergence/>
</rules>
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
<execution>
<id>enforce-ban-duplicate-classes</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<banDuplicateClasses>
<ignoreClasses>
<ignoreClass>javax.*</ignoreClass>
<ignoreClass>org.junit.*</ignoreClass>
<ignoreClass>net.sf.cglib.*</ignoreClass>
<ignoreClass>org.apache.commons.logging.*</ignoreClass>
<ignoreClass>org.springframework.remoting.rmi.RmiInvocationHandler</ignoreClass>
</ignoreClasses>
<findAllDuplicates>true</findAllDuplicates>
</banDuplicateClasses>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>extra-enforcer-rules</artifactId>
<version>1.0-beta-6</version>
</dependency>
</dependencies>
</plugin>