上篇文章中,小黑哥分析 Maven 依賴沖突分為兩類:
- 項目同一依賴應用,存在多版本,每個版本同一個類,可能存在差異。
- 項目不同依賴應用,存在包名,類名完全一樣的類。
第二種情況,往往是這個場景,本地/測試環境運行的都是好好的,上線之后測試就是不行。

這其實與 JVM 類加載有關,本地/測試環境加載正確類,而生產環節加載錯的類,為什么會這樣?
主要有兩個原因:
- 同一個類只會被加載器加載一次
- 不同環境,類的加載順序不同
同一個類只會被加載器加載一次
JVM 類加載具有緩存機制,每個類加載的時候首先檢查一遍,類是否被當前類加載器加載。若未被加載,先交給其父類加載器加載,父類加載器不能加載,才會交給當前類加載器。
當前類加載器加載完成之后,將會將其緩存起來。

類加載的核心源碼位於 ClassLoader#loadClass:

① 處將會檢查ClassLoader#findLoadedClass 最終將會調用 ClassLoader#findLoadedClass0,這是一個 native 方法,最終將會根據類名加類加載器為鍵值查找緩存。
每個類加載器負責的加載范圍都不一樣:
BootstrapClassLoader引導類加載加載最核心的類庫,如$JAVA_HOME/jre/lib/ExtClassLoader擴展類加載器負責加載$JAVA_HOME/jre/lib/ext下的一些擴展類AppClassLoader應用類加載器將加載classpath指定的類。
我們運行的應用依賴的各種類,一般將會由 AppClassLoader 記載,同名類被加載后,下次碰到就不會再被加載。
畫外音:利用緩存加快查詢速度
不同環境,類的加載順序不同
Java 可以使用 -classpath 參數指定依賴類所在位置。
類的加載順序可以通過以下方式指定:
java -classpath a.jar:b.jar:c.jar xx.xx.Main
上面這種方式,類加載首先會從 a.jar 中查找相關類,找不到才會繼續往后查找。所以可以通過這種方式可以指定使用哪個 jar 包內同名類。
但是這種方式有點繁瑣,如果依賴 100 個 jar 包,需要全部寫上去。
所以生產環境可以使用使用 shell 命令將 jar 拼接起來:
LIB_DIR=lib
LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"$0}'|tr "\n" ":"`
另外 java 支持通配符的寫法:
java -classpath './*' xx.xx.Main
這種方式的加載順序將會受到底層系統文件加載順序影響。
復現依賴沖突
假設我們現在應用依賴如下:

A 應用依賴 B、C,且 B,C 中存在同包同名類 org.example.App,代碼如下:

如果指定 jar 包順序啟動應用:
# A,B,C 放置同一文件夾下
java -classpath A-1.0-SNAPSHOT.jar:B-1.0-SNAPSHOT.jar:C-1.0-SNAPSHOT.jar org.example.ClassA
日志輸出如下:

改變 B ,C 順序:

類加載器的類的查找順序將會通過 classpath 指定順序從前往后查找。
如果使用通配符啟動:
java -classpath './*' org.example.ClassA
這種情況 jvm 到底加載那個類就成了薛定諤的類了,運行之前無法確定加載類來自哪個 jar 包。

使用 verbose:class 打印加載類
我們可以在 jvm 啟動腳本加入如下參數 -verbose:class,然后重啟,日志里會打印出每個類的加載信息。
java -verbose:class -classpath './*' xx.xx.Main
日志輸出如下:

通過這種方式可以看到加載類來源於哪個Jar包。
不過這種方式需要重啟應用,對生產系統來說,影響還是比較大,不太優雅。
Arthas 查到來源類
阿里開源項目 Arthas sc 命令可以用來查找加載類的信息。。
sc 命令是 Search-Class 簡寫,這個命令能搜索出已經加載到 JVM 中的 Class 信息,支持參數如下表格所示。

程序啟動之后,啟動 arthas,進入 A 應用。
運行如下命令:
sc -d org.example.App
輸出結果如下 :

code-source 顯示當前查找類 org.example.App 來自的 C。
另外我們可以 jad 命令反編譯類,在線查看源碼。

總結
這篇文章主要解釋應用中存在多個同名類,環境不同,類加載不同的原因。接着介紹了兩種快速查找運行應用依賴類來源的方法。
當定位到了沖突類的來源,我們可以顯示指定 classpath jar 包的順序,指定類加載的順序。但這只是暫時解決問題。本質上依賴沖突的問題,還是需要深層次排除的。
歡迎關注我的公眾號:程序通事,獲得日常干貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn
