上篇文章中,小黑哥分析 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=libLIB_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日誌輸出如下:
不過這種方式需要重啟應用,對生產系統來說,影響還是比較大,不太優雅。
Arthas 查到來源類阿里開源專案 Arthas sc 命令可以用來查詢載入類的資訊。。
sc 命令是 Search-Class 簡寫,這個命令能搜尋出已經載入到 JVM 中的 Class 資訊,支援引數如下表格所示。
程式啟動之後,啟動 arthas,進入 A 應用。
執行如下命令:
sc -d org.example.App輸出結果如下 :
code-source 顯示當前查詢類 org.example.App 來自的 C。
另外我們可以 jad 命令反編譯類,線上檢視原始碼。
總結這篇文章主要解釋應用中存在多個同名類,環境不同,類載入不同的原因。接著介紹了兩種快速查詢執行應用依賴類來源的方法。
當定位到了衝突類的來源,我們可以顯示指定 classpath jar 包的順序,指定類載入的順序。但這只是暫時解決問題。本質上依賴衝突的問題,還是需要深層次排除的。