一翔曲、前言
早前,筆者在碰到ClassNotFoundException的異常也是一臉懵逼蛀恩,但是這個類確實存在于我們的項目中疫铜,覺很離奇茂浮,就厚著臉皮去問組里的大神勛哥,勛哥開始一臉鄙視,心想這個居然都不知道席揽,抵不過我的再三追問顽馋,勛哥拋出一句tree看一下,雖然我還是不懂幌羞,但是不好意思再問下去寸谜,感覺再問下去會被打死。事后勛哥過來問我属桦,那個依賴沖突的問題解決了沒有熊痴,解決了的話,給大家分享一下解決方案聂宾,后來因為各種原因果善,一直沒有完成那個分享,所以借這篇文章補上吧系谐。
二巾陕、正文
2.1 表象
Jar包沖突作為一個老生常談的問題,幾乎每一個程序員都會遇到纪他。jar包沖突通常發(fā)生在程序編譯時或運行時鄙煤。主要分為兩類:一類比較直觀也是最常見的,在運行時拋出各種異常茶袒,還有一類比較隱晦梯刚,它不會直接報錯,但是程序的行為卻和預(yù)期不一致薪寓,羅列如下:
- java.lang.ClassNotFoundException乾巧,即找不到指定的java類。
- java.lang.NoSuchMethodError预愤,即找不到指定的方法沟于。
- java.lang.NoClassDefFoundError,即找不到指定的java類(運行時報錯)植康。
- 沒有異常旷太,但是程序的行為和預(yù)期不一致。
如果有上述行為销睁,就很有可能出現(xiàn)了包沖突的問題供璧。
2.2 原理
在正式談?wù)撊绾谓鉀Q這一問題之前,我們不妨先來研究下為什么會出現(xiàn)包沖突的問題冻记。很顯然睡毒,當我們使用Maven作為包依賴的管理工具的時候,如果我們直接或者間接的引入了groupId和artifactId都相同的包時冗栗,maven究竟是怎么選擇最終使用哪個version的包來進行打包的呢演顾?
2.2.1 傳遞依賴沖突
依賴傳遞:
情形1:如果A依賴B供搀,并且A頁依賴C,那么引入A钠至,意味著B和C都會被引入葛虐。
情形2:如果A依賴B,B依賴C棉钧,那么引入A屿脐,意味著B和C都會被引入。
Maven引入的傳遞性依賴機制宪卿,一方面大大簡化和方便了依賴聲明的诵,另一方面,大部分情況下我們只需要關(guān)心項目的直接依賴是什么佑钾,而不用考慮這些直接依賴會引入什么傳遞性依賴奢驯。但有時候,這種傳遞性依賴會造成問題次绘。
例如瘪阁,項目中有這樣的依賴關(guān)系:A->B->D(1.0)、A->C->D(1.2)邮偎,D是B和C的傳遞性依賴管跺,但是兩條依賴路徑上有兩個版本的D,最終哪個D會被Maven解析使用呢禾进?
Maven最終選擇哪個這里暫不給結(jié)論豁跑,假設(shè)最終選擇的是D-1.0,但是我們在代碼編寫的時候使用到了與D1.0中就有的某個類泻云,但是該類在D1.2中新增的某個方法的時候艇拍,在編寫時代碼不會報錯,但是一旦編譯運行就會報錯java.lang.NoSuchMethodError
宠纯。
2.2.2 依賴調(diào)解原則
2.2.2.1 路徑最近者優(yōu)先原則
Maven依賴調(diào)解(Dependency Mediation)的第一原則是:路徑最近者優(yōu)先卸夕。
如果項目的依賴圖如下圖所示:D(1.0)的路徑長度為2,而D(1.2)的路徑長度為3婆瓜,因此D(1.0)會被解析使用快集。
2.2.2.2 第一聲明者優(yōu)先原則
依賴調(diào)解第一原則不能解決所有問題。例如下面這個例子廉白,D的兩個版本到達A的兩條依賴路徑長度都為2个初。那么到底誰會被解析使用呢?在Maven 2.0.8及之前的版本中猴蹂,結(jié)果是不確定的院溺;但是從Maven 2.0.9開始,為了盡可能避免構(gòu)建的不確定性磅轻,Maven定義了依賴調(diào)解的第二原則:第一聲明者優(yōu)先珍逸,即需要找到在pom文件聲明中逐虚,依賴B的聲明是寫在了C的前面還是后面,如果依賴B的聲明寫在前面弄息,那么D-1.0有效,否則就是D1-2有效勤婚。這種原則會解決不確定性的問題摹量,但是有時候我們需要使用到類的功能也會因為這一原則而使用不了。
2.2.3 小結(jié)
在大多數(shù)時候馒胆,依賴沖突可能并不會對系統(tǒng)造成什么異常缨称,因為Maven始終選擇了一個Jar包來使用。但是祝迂,不排除在某些特定條件下睦尽,會出現(xiàn)類似找不到類的異常,所以型雳,只要存在依賴沖突当凡,在我看來,最好還是解決掉纠俭,不要給系統(tǒng)留下隱患沿量。
2.3 解決方案
2.3.1 尋找沖突依賴
2.3.1.1 mvn dependency : tree指令
第一步:找到傳遞依賴的鬼出在哪里?
dependency:tree是把照妖照冤荆,pom.xml用它照照朴则,所有傳遞性依賴都將無處遁形,并且會以層級樹方式展現(xiàn)钓简,非常直觀乌妒。以下就是執(zhí)行dependency:tree后的一個輸出:
[INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ euler-foundation ---
[INFO] com.hsit:euler-foundation:jar:0.9.0.1-SNAPSHOT
[INFO] +- com.rop:rop:jar:1.0.1:compile
[INFO] | +- org.slf4j:slf4j-api:jar:1.7.5:compile
[INFO] | +- org.slf4j:slf4j-log4j12:jar:1.7.5:compile
[INFO] | +- log4j:log4j:jar:1.2.16:compile
[INFO] | +- commons-lang:commons-lang:jar:2.6:compile
[INFO] | +- commons-codec:commons-codec:jar:1.6:compile
[INFO] | +- javax.validation:validation-api:jar:1.0.0.GA:compile
[INFO] | +- org.hibernate:hibernate-validator:jar:4.2.0.Final:compile
[INFO] | +- org.codehaus.jackson:jackson-core-asl:jar:1.9.5:compile
[INFO] | +- org.codehaus.jackson:jackson-mapper-asl:jar:1.9.5:compile
[INFO] | +- org.codehaus.jackson:jackson-jaxrs:jar:1.9.5:compile
[INFO] | +- org.codehaus.jackson:jackson-xc:jar:1.9.5:compile
[INFO] | \- com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.2.3:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-core:jar:2.2.3:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.2.3:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.2.3:compile
[INFO] | +- com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.2.3:compile
[INFO] | \- org.codehaus.woodstox:stax2-api:jar:3.1.1:compile
[INFO] | \- javax.xml.stream:stax-api:jar:1.0-2:compile
剛才吹噓dependency:tree時,我用到了“無處遁形”外邓,其實有時你會發(fā)現(xiàn)簡單地用dependency:tree往往并不能查看到所有的傳遞依賴撤蚊。不過如果你真的想要看所有的,必須得加一個-Dverbose
參數(shù)损话,這時就必定是最全的了拴魄。
內(nèi)容太多,眼花繚亂席镀,有沒有好法呢匹中?當然有了,加上Dincludes
或者Dexcludes
制定小包含或者排除的包豪诲,dependency:tree就會幫你過濾出來:
引用
Dincludes=org.springframework:spring-tx
過濾串使用groupId:artifactId:version
的方式進行過濾顶捷,可以不用寫全,例如:
mvn dependency:tree -Dverbose -Dincludes=asm:asm
就會出來asm依賴包的分析信息:
[INFO] --- maven-dependency-plugin:2.1:tree (default-cli) @ ridge-test ---
[INFO] com.ridge:ridge-test:jar:1.0.2-SNAPSHOT
[INFO] +- asm:asm:jar:3.2:compile
[INFO] \- org.unitils:unitils-dbmaintainer:jar:3.3:compile
[INFO] \- org.hibernate:hibernate:jar:3.2.5.ga:compile
[INFO] +- cglib:cglib:jar:2.1_3:compile
[INFO] | \- (asm:asm:jar:1.5.3:compile - omitted for conflict with 3.2)
[INFO] \- (asm:asm:jar:1.5.3:compile - omitted for conflict with 3.2)
[INFO] ------------------------------------------------------------------------
對asm有依賴有一個直接的依賴(asm:asm:jar:3.2)還有一個傳遞進入的依賴(asm:asm:jar:1.5.3)
2.3.1.2 Maven依賴結(jié)構(gòu)圖
可以使用IDEA提供的方法——Maven依賴結(jié)構(gòu)圖屎篱,打開Maven窗口服赎,選擇Dependencies葵蒂,然后點擊那個圖標(Show Dependencies)或者使用快捷鍵(Ctrl+Alt+Shift+U),即可打開Maven依賴關(guān)系結(jié)構(gòu)圖
在圖中重虑,我們可以看到有一些紅色的實線践付,這些紅色實線就是依賴沖突,藍色實線則是正常的依賴缺厉。
2.3.1.3 IDEA Maven Helper插件
首先永高,按照常規(guī)的IDEA 插件安裝的方式安裝插件Maven Helper:
安裝的過程可能會出現(xiàn)下面的報錯,是因為插件和idea的版本不兼容提针,換個插件版本就好了命爬。
在插件安裝好之后,我們打開pom.xml文件辐脖,在底部會多出一個Dependency Analyzer選項饲宛。
點開這個選項,找到?jīng)_突嗜价,點擊右鍵艇抠,然后選擇Exclude即可排除沖突版本的Jar包。
- Conflicts:顯示所有的沖突的依賴
- All dependencys as List:以列表的形式顯示所有的依賴
- All dependencys as tree:以樹的形式顯示所有的依賴
注意:
同一個jar包可能需要執(zhí)行多次Exclude操作久锥,因為可能有多處沖突练链。
執(zhí)行Exclude之后需要點擊"Refresh"刷新一下,才能確定是否依然存在沖突奴拦。
2.3.2 處理沖突依賴
2.3.2.1 加載提前
在清楚了Maven的依賴調(diào)解規(guī)則后媒鼓,我可以很自然地想到解決方案,就是把我們需要的版本的路徑縮短或者聲明提前错妖。如下圖绿鸣,比如我們明確需要使用D-1.2,那么我們可以明確在pom依賴中暂氯,手動引入D-1.2包潮模,并且將D-1.2的依賴聲明寫在依賴A的前面即可:
2.3.2.2 排除依賴
也就是使用exclusions元素聲明排除其中一個依賴,exclusions可以包含一個或者多個exclusion子元素痴施,因此可以排除一個或者多個傳遞性依賴擎厢。需要注意的是,聲明exclusion的時候只需要groupId和artifactId辣吃,而不需要version元素动遭,這是因為只需要groupId和artifactId就能唯一定位依賴圖中的某個依賴。換句話說神得,Maven解析后的依賴中厘惦,不可能出現(xiàn)groupId和artifactId相同,但是version不同的兩個依賴哩簿。
<dependency>
<groupId>com.alibaba.aecp</groupId>
<artifactId>logger-formatter</artifactId>
<version>${logger-formatter.version}</version>
<exclusions>
<exclusion>
<groupId>com.taobao.eagleeye</groupId>
<artifactId>eagleeye-core</artifactId>
</exclusion>
</exclusions>
</dependency>
2.3.2.3 升級父節(jié)點
使用上面三種方法都有一個前提宵蕉,那就是你選定的version是可以兼容兩個沖突的jar酝静。但是兩個jar不兼容的話,針對這種情況羡玛, 去掉任何一個依賴别智,都會出現(xiàn)異常。
針對這種情況稼稿, 去掉任何一個依賴薄榛,都會出現(xiàn)異常 。接口升級引入了新二方包渺杉,導(dǎo)致項目中間接依賴了slf4j-api:1.5.11和slf4j-api:1.7.5蛇数,結(jié)果這兩個包還不兼容挪钓,1.7.5新增了一些類是越,同時把1.5.11中一些方法簽名改了,結(jié)果這些變動的類和方法都被引用了碌上。最后倚评,使用maven helper查看1.5.11的整個依賴樹,找到其父節(jié)點馏予,升級其父節(jié)點version天梧,這樣父節(jié)點依賴的slf4j-api的version也會跟著變,找到一個能兼容的版本即可。
2.3.2.4 全路徑?jīng)_突
還有一種特殊的沖突,多個dependency的groupID或artifactID不同(或兩者都不同)秽荤,但包中存在全路徑類名相同的類Java類加載器根據(jù)classpath加載類時灿渴,根據(jù)classpath中jar包出現(xiàn)的先后順序進行查找類并緩存,后面jar包中的類不使用被去。這個時候的常見異常就是NoSuchMethodException,NoClassDefFoundError,ClassNotFoundException挫酿,NoSuchMethodError等。
如果其中一個jar是我們不需要的愕难,那么排除它就行了早龟。但是,如果這個jar被很多dependency依賴猫缭,你需要一個個去寫exclusions是不是很麻煩葱弟。這時我們可以直接在pom中添加一個空依賴(和想要去掉的jar的groupID,artifactID相同猜丹,但是version不同的一個空項目打包上傳到遠程倉庫中)翘悉。
<!-- ================================================= -->
<!-- 排除依賴 -->
<!-- ================================================= -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>999-not-exist-v3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>999-not-exist</version>
</dependency>