一次Maven依赖冲突采坑,把依赖调解、类加载彻底整明白了
今年年初的時(shí)候,閱讀過(guò)《Maven實(shí)戰(zhàn)》,當(dāng)時(shí)有了解到Maven可以依賴調(diào)解,即當(dāng)包版本不一致時(shí),會(huì)根據(jù)一定規(guī)則選擇相應(yīng)的包來(lái)加載,從而避免沖突。當(dāng)時(shí)不解的是既然Maven都能解決沖突,為何還經(jīng)常聽到“發(fā)生了依賴沖突”,沖突不是解決了嗎,還存在什么問題呢?直到這周在工作中自己遇到了,就明白是咋回事了。下面先從我的實(shí)際經(jīng)歷說(shuō)起。
1. Maven依賴沖突經(jīng)歷
我在Y模塊中,寫了一個(gè)Encryptor類,主要是使用了DigestUtils、MessageDigest、HmacUtils等類對(duì)字符串進(jìn)行加密(下面代碼是隨便寫的,只表示使用到了這些類),如下:
import?org.apache.commons.codec.binary.Hex; import?org.apache.commons.codec.digest.DigestUtils; import?org.apache.commons.codec.digest.HmacUtils;import?java.nio.charset.StandardCharsets; import?java.security.MessageDigest;public?class?Encryptor?{public?String?encrype(String?s)?{MessageDigest?sha256Digest?=?DigestUtils.getSha256Digest();String?result?=?Hex.encodeHexString(sha256Digest.digest(s.getBytes(StandardCharsets.UTF_8)));return?Hex.encodeHexString(HmacUtils.getHmacSha256(result.getBytes()).doFinal(result.getBytes()));}public?static?void?main(String[]?args)?{Encryptor?encryptor?=?new?Encryptor();String?s?=?"test";String?result?=?encryptor.encrype(s);System.out.println(result);}/**output:?fdd04dcac94e9803a72e4268141f773e2024a8fe46ba19a263be22c5ca83e931**/}執(zhí)行單元測(cè)試可以正常運(yùn)行。但是當(dāng)整個(gè)應(yīng)用啟動(dòng)時(shí),則會(huì)報(bào)IllegalAccessError錯(cuò)誤。
應(yīng)用啟動(dòng)報(bào)錯(cuò)IllegalAccessError在Y模塊下的單元測(cè)試運(yùn)行時(shí)不會(huì)報(bào)錯(cuò),但是當(dāng)整個(gè)應(yīng)用啟動(dòng),作為程序入口的X模塊,調(diào)用Y模塊中的Encryptor時(shí),發(fā)生了IllegalAccessError報(bào)錯(cuò)。根據(jù)圖中的具體報(bào)錯(cuò)信息,是說(shuō)沒有權(quán)限訪問getSha256Digest方法,我Ctrl+B點(diǎn)進(jìn)getSha256Digest方法查看,如下:
getSha256Digest是publicgetSha256Digest方法是public的訪問級(jí)別,我一臉懵。由于這個(gè)方法很簡(jiǎn)單,既然報(bào)錯(cuò),那我就索性不用了,換成下面這種寫法。
public?String?encrype(String?s)?{try?{MessageDigest?sha256Digest?=?MessageDigest.getInstance("SHA-256");String?result?=?Hex.encodeHexString(sha256Digest.digest(s.getBytes(StandardCharsets.UTF_8)));return?Hex.encodeHexString(HmacUtils.getHmacSha256(result.getBytes()).doFinal(result.getBytes()));}?catch?(NoSuchAlgorithmException?e)?{e.printStackTrace();return?"error";} }又報(bào)錯(cuò)了,好吧,真是躲不過(guò)了!報(bào)錯(cuò)如下:
應(yīng)用啟動(dòng)報(bào)錯(cuò)ClassNotFoundException這次報(bào)的是ClassNotFoundException,HmacUtils這個(gè)類找不到。可是我Ctrl+B進(jìn)去,這個(gè)類好好的就在那里啊。這時(shí)我才把注意力集中在思考是不是發(fā)生了Maven依賴沖突。我打開pom.xml,用Dependency Analyzer查看,果然我使用的commons-codec包發(fā)生了沖突。
X模塊的依賴樹在Y模塊中,依賴關(guān)系:Y -> B -> C -> commons-codec-1.10。而在X模塊中,引用了A包:X -> A -> commons-codec-1.6,也引用了Y模塊:X -> Y -> B -> C -> commons-codec-1.10。可見commons-codec包有兩個(gè)版本1.6和1.10,所以Maven會(huì)進(jìn)行依賴調(diào)解,第一原則是“路徑最短者優(yōu)先”,自然只會(huì)使用1.6版本的包。而我再去查看1.6的包下,getSha256Digest方法是private的訪問級(jí)別,HmacUtils這個(gè)類也不存在。解釋了之前的報(bào)錯(cuò)。解決該沖突,通過(guò)排除依賴便能解決了,將A包下的commons-codec排除,如下:
<dependencies><dependency><groupId>com.chaycao.maven.dependency</groupId><artifactId>A</artifactId><version>1.0-SNAPSHOT</version><exclusions><exclusion><artifactId>commons-codec</artifactId><groupId>commons-codec</groupId></exclusion></exclusions></dependency><dependency><groupId>com.chaycao.maven.dependency</groupId><artifactId>Y</artifactId><version>1.0-SNAPSHOT</version></dependency> </dependencies>排除后,這時(shí)將只有1.10版本的包,程序也可以正常運(yùn)行了。
2. 為什么需要Maven依賴調(diào)解
問題已經(jīng)解決了,大家是不是也明白了,為什么依賴沖突會(huì)常導(dǎo)致發(fā)生NoClassDefFoundError、NoSuchMethodException、IllegalAccessError等錯(cuò)誤。雖然Y模塊在編譯時(shí),由于引入了commons-codec 1.10能正常編譯,但是在運(yùn)行時(shí),由于依賴沖突,只加載了1.6版本的包,所以不能正常運(yùn)行。
注意:代碼的編譯僅僅是編譯當(dāng)前的代碼。編譯成功后,最后能否正常運(yùn)行,還要取決于運(yùn)行時(shí)的環(huán)境是否等同或兼容編譯時(shí)環(huán)境。
下面我們想想為什么需要Maven依賴調(diào)解,如果不調(diào)解行不行。
當(dāng)使用Maven的過(guò)程中,如果同時(shí)引入了groupId和artifactId相同而version不同的包時(shí),Maven會(huì)認(rèn)為發(fā)生了依賴沖突,將進(jìn)行依賴調(diào)解,通過(guò)兩個(gè)原則決定使用哪個(gè)版本的包:第一原則,路徑最近者優(yōu)先,如前文。如果路徑相同,則使用第二原則,在pom中第一聲明者優(yōu)先。而當(dāng)我們?cè)邳c(diǎn)擊Run運(yùn)行時(shí),classpath中將只會(huì)有一個(gè)明確版本的包。
思考一下。Java在運(yùn)行時(shí),是否能引入版本不同的包。其實(shí)這個(gè)問題是在問,java命令的classpath參數(shù)中能不能有多個(gè)版本不同的包,當(dāng)然是可以的。classpath參數(shù)的是用于指示JVM如何搜索class文件,當(dāng)你在classpath中指定的路徑下有多個(gè)版本不同的包,JVM都會(huì)去jar包下搜索class文件進(jìn)行加載,而至于class能不能成功加載,則在于ClassLoader的邏輯,當(dāng)同名類被加載時(shí),則不會(huì)再被加載,即同一個(gè)類只會(huì)被加載一次。這也意味,當(dāng)有多個(gè)版本不同的包時(shí),包在classpath中的順序,決定了哪個(gè)包中的類能先被加載。而這樣具有不確定性。因?yàn)樵谏a(chǎn)環(huán)境下通常使用shell命令將jar包拼接:
LIB_DIR=lib LIB_JARS=`ls?$LIB_DIR|grep?.jar|awk?'{print?"'$LIB_DIR'/"$0}'|tr?"\n"?":"`不同環(huán)境下得到的jar包順序可能是不同的。而Maven依賴調(diào)解將使得只有一個(gè)明確版本的包參與構(gòu)建,從而避免不確定性。
3. 排查在線問題的利器-Arthas
Arthas,早有聽說(shuō),但一直未使用過(guò),這次我嘗試了下,覺得確實(shí)可以,安利下。對(duì)于前文說(shuō)的依賴沖突情況,當(dāng)發(fā)生IllegalAccessError報(bào)錯(cuò)時(shí),可以通過(guò)Arthas直接查看運(yùn)行情況下的DigestUtils。我們把代碼變?yōu)樽畛醯那闆r,且在Main類中加個(gè)死循環(huán),為了讓程序不死掉,以通過(guò)Arthas觀察。
public?class?Main?{public?static?void?main(String[]?args)?{while?(true)?{try?{Encryptor?encryptor?=?new?Encryptor();String?s?=?"1234567890";String?result?=?encryptor.encrype(s);System.out.println(result);}?catch?(Throwable?e)?{}}}}打開Arthas,連接上我們的程序(可以通過(guò)官方教程學(xué)習(xí)),然后通過(guò)sc命令查看DigestUtils:
[arthas@32328]$?sc?-d?org.apache.commons.codec.digest.DigestUtilsclass-info????????org.apache.commons.codec.digest.DigestUtilscode-source???????/D:/mavenrepo/commons-codec/commons-codec/1.6/commons-codec-1.6.jarname??????????????org.apache.commons.codec.digest.DigestUtilsisInterface???????falseisAnnotation??????falseisEnum????????????falseisAnonymousClass??falseisArray???????????falseisLocalClass??????falseisMemberClass?????falseisPrimitive???????falseisSynthetic???????falsesimple-name???????DigestUtilsmodifier??????????publicannotationinterfacessuper-class???????+-java.lang.Objectclass-loader??????+-sun.misc.Launcher$AppClassLoader@58644d46+-sun.misc.Launcher$ExtClassLoader@24e74ca5classLoaderHash???58644d46可以從code-source中清晰的查到DigestUtils是哪個(gè)包下的Class,這時(shí)就該意識(shí)到發(fā)生了依賴沖突問題。
而通過(guò)jad命令,還能反編譯,在線看代碼。好用!
參考
Arthas 實(shí)戰(zhàn),助你解決同名類依賴沖突問題(https://www.cnblogs.com/goodAndyxublog/p/12424734.html)
Maven依賴沖突問題原理簡(jiǎn)析(https://blog.csdn.net/qq_27529917/article/details/79741607)
重新看待Jar包沖突問題及解決方案(http://www.yangbing.club/2017/07/15/solution-for-jar-conflicts/)
有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的一次Maven依赖冲突采坑,把依赖调解、类加载彻底整明白了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springboot系列八、spring
- 下一篇: Leetcode-937-Reorder