一次Maven依赖冲突采坑,把依赖调解、类加载彻底整明白了
今年年初的時候,閱讀過《Maven實戰》,當時有了解到Maven可以依賴調解,即當包版本不一致時,會根據一定規則選擇相應的包來加載,從而避免沖突。當時不解的是既然Maven都能解決沖突,為何還經常聽到“發生了依賴沖突”,沖突不是解決了嗎,還存在什么問題呢?直到這周在工作中自己遇到了,就明白是咋回事了。下面先從我的實際經歷說起。
1. Maven依賴沖突經歷
我在Y模塊中,寫了一個Encryptor類,主要是使用了DigestUtils、MessageDigest、HmacUtils等類對字符串進行加密(下面代碼是隨便寫的,只表示使用到了這些類),如下:
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**/}執行單元測試可以正常運行。但是當整個應用啟動時,則會報IllegalAccessError錯誤。
應用啟動報錯IllegalAccessError在Y模塊下的單元測試運行時不會報錯,但是當整個應用啟動,作為程序入口的X模塊,調用Y模塊中的Encryptor時,發生了IllegalAccessError報錯。根據圖中的具體報錯信息,是說沒有權限訪問getSha256Digest方法,我Ctrl+B點進getSha256Digest方法查看,如下:
getSha256Digest是publicgetSha256Digest方法是public的訪問級別,我一臉懵。由于這個方法很簡單,既然報錯,那我就索性不用了,換成下面這種寫法。
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";} }又報錯了,好吧,真是躲不過了!報錯如下:
應用啟動報錯ClassNotFoundException這次報的是ClassNotFoundException,HmacUtils這個類找不到。可是我Ctrl+B進去,這個類好好的就在那里啊。這時我才把注意力集中在思考是不是發生了Maven依賴沖突。我打開pom.xml,用Dependency Analyzer查看,果然我使用的commons-codec包發生了沖突。
X模塊的依賴樹在Y模塊中,依賴關系: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包有兩個版本1.6和1.10,所以Maven會進行依賴調解,第一原則是“路徑最短者優先”,自然只會使用1.6版本的包。而我再去查看1.6的包下,getSha256Digest方法是private的訪問級別,HmacUtils這個類也不存在。解釋了之前的報錯。解決該沖突,通過排除依賴便能解決了,將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>排除后,這時將只有1.10版本的包,程序也可以正常運行了。
2. 為什么需要Maven依賴調解
問題已經解決了,大家是不是也明白了,為什么依賴沖突會常導致發生NoClassDefFoundError、NoSuchMethodException、IllegalAccessError等錯誤。雖然Y模塊在編譯時,由于引入了commons-codec 1.10能正常編譯,但是在運行時,由于依賴沖突,只加載了1.6版本的包,所以不能正常運行。
注意:代碼的編譯僅僅是編譯當前的代碼。編譯成功后,最后能否正常運行,還要取決于運行時的環境是否等同或兼容編譯時環境。
下面我們想想為什么需要Maven依賴調解,如果不調解行不行。
當使用Maven的過程中,如果同時引入了groupId和artifactId相同而version不同的包時,Maven會認為發生了依賴沖突,將進行依賴調解,通過兩個原則決定使用哪個版本的包:第一原則,路徑最近者優先,如前文。如果路徑相同,則使用第二原則,在pom中第一聲明者優先。而當我們在點擊Run運行時,classpath中將只會有一個明確版本的包。
思考一下。Java在運行時,是否能引入版本不同的包。其實這個問題是在問,java命令的classpath參數中能不能有多個版本不同的包,當然是可以的。classpath參數的是用于指示JVM如何搜索class文件,當你在classpath中指定的路徑下有多個版本不同的包,JVM都會去jar包下搜索class文件進行加載,而至于class能不能成功加載,則在于ClassLoader的邏輯,當同名類被加載時,則不會再被加載,即同一個類只會被加載一次。這也意味,當有多個版本不同的包時,包在classpath中的順序,決定了哪個包中的類能先被加載。而這樣具有不確定性。因為在生產環境下通常使用shell命令將jar包拼接:
LIB_DIR=lib LIB_JARS=`ls?$LIB_DIR|grep?.jar|awk?'{print?"'$LIB_DIR'/"$0}'|tr?"\n"?":"`不同環境下得到的jar包順序可能是不同的。而Maven依賴調解將使得只有一個明確版本的包參與構建,從而避免不確定性。
3. 排查在線問題的利器-Arthas
Arthas,早有聽說,但一直未使用過,這次我嘗試了下,覺得確實可以,安利下。對于前文說的依賴沖突情況,當發生IllegalAccessError報錯時,可以通過Arthas直接查看運行情況下的DigestUtils。我們把代碼變為最初的情況,且在Main類中加個死循環,為了讓程序不死掉,以通過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,連接上我們的程序(可以通過官方教程學習),然后通過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是哪個包下的Class,這時就該意識到發生了依賴沖突問題。
而通過jad命令,還能反編譯,在線看代碼。好用!
參考
Arthas 實戰,助你解決同名類依賴沖突問題(https://www.cnblogs.com/goodAndyxublog/p/12424734.html)
Maven依賴沖突問題原理簡析(https://blog.csdn.net/qq_27529917/article/details/79741607)
重新看待Jar包沖突問題及解決方案(http://www.yangbing.club/2017/07/15/solution-for-jar-conflicts/)
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的一次Maven依赖冲突采坑,把依赖调解、类加载彻底整明白了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springboot系列八、spring
- 下一篇: Leetcode-937-Reorder