天天看點

從Jar包沖突搞到類加載機制,就是這麼霸氣

接手了一套比較有年代感的系統,計劃把重構及遇到的問題寫成系列文章,老樹發新枝,重溫一些實戰技術,分享給大家。【重構01篇】,給大家講講Jar包沖突及原理。

背景

目前市面上項目管理要麼是基于Maven,要麼是基于Gradle,最近接手了一套純手動添加jar包的項目。

對于純手動添加jar包的項目已經是多年前的方式了,現在工作三五年的技術人員可能都沒有經曆過。就是把項目中所需的jar包挨個找出來,添加到一個lib目錄中,在IDE中再将jar包依賴手動添加上。

這種方式來添加jar包依賴,不僅費事,而且很容易出現jar包沖突,同時分析沖突手段,隻能憑借經驗。

最近就遇到這樣一種情況:一個項目在開發者A的環境中可以正常啟動,在B那裡就無法啟動,而異常資訊是找不到什麼什麼類。

稍微有一些開發經驗的人,馬上就可以斷定是jar包沖突導緻。下面就看看如何解決及引申出來的知識點。

臨時解決方案

由于暫時無法對項目進行大範圍重構,也不敢輕易将Jar包進行替換更新。隻能采用臨時的手段來進行解決。

這裡總結幾個步驟以備不時之需,通常也是解決Jar依賴問題的小技巧。

第一:在IDE中查找異常中找不到的類。比如IDEA MAC作業系統,我用的快捷鍵是command + shift + n。

從Jar包沖突搞到類加載機制,就是這麼霸氣

以Assert類為例,可以看到有很多包都包含了Assert,但啟動程式卻報找不到該類的某個方法,問題基本上就出在Jar包沖突上了。

第二,定位到Jar包沖突之後,找到系統本應該使用的Jar包。

比如這裡需要使用的spring-core中的類,而不spring.jar中的類。那麼,就可以利用JVM的類加載順序機制,讓JVM先加載spring-core的jar包。

知識點:在同一目錄下的jar包,JVM是按照jar包的先後順序進行加載,一旦一個全路徑名相同的類被加載之後,後面再有相同的類便不會進行加載了。

是以,臨時解決方案就是調整JVM編譯(加載)Jar包的順序。這個在Eclipse和Idea中都有支援,可以手動進行調整。

Eclipse中調整方式:

從Jar包沖突搞到類加載機制,就是這麼霸氣

Idea中調整方式:

從Jar包沖突搞到類加載機制,就是這麼霸氣

把需要優先加載的jar包往上調整,這樣就可以優先加載它,總算是臨時解決了jar包沖突的問題。

類加載機制的延伸

上面隻是受限于項目現狀的臨時解決方案,最終肯定是要進行改造更新的,基于Maven或Gradle進行Jar包管理,同時解決掉Jar包沖突的問題的。

在這個臨時解決方案,涉及到一個JVM的關鍵知識點:JVM的類加載器的隔離問題及雙親委派機制。如果沒有JVM類加載機制的相關知識,可能連上面的臨時方案都無法想到。

類加載器的隔離問題

每個類裝載器都有一個自己的命名空間用來儲存已裝載的類。當一個類裝載器裝載一個類時,它會通過儲存在命名空間裡的類全局限定名(

Fully Qualified Class Name

) 進行搜尋來檢測這個類是否已經被加載了。

JVM

對類唯一的識别是

ClassLoader id

+

PackageName

+

ClassName

,是以一個運作程式中是有可能存在兩個包名和類名完全一緻的類的。并且如果這兩個類不是由一個

ClassLoader

加載,是無法将一個類的執行個體強轉為另外一個類的,這就是

ClassLoader

隔離性。

為了解決類加載器的隔離問題,

JVM

引入了雙親委派機制。

雙親委派機制

雙親委派機制的核心有兩點:第一,自底向上檢查類是否已加載;其二,自頂向下嘗試加載類。

從Jar包沖突搞到類加載機制,就是這麼霸氣

類加載器通常有四類:啟動類加載器、拓展類加載器、應用程式類加載器和自定義類加載器。

暫且不考慮自定義類加載器,JDK自帶類加載器具體執行過程如下:

第一:當

AppClassLoader

加載一個

class

時,會把類加載請求委派給父類加載器

ExtClassLoader

去完成;

第二:當

ExtClassLoader

加載一個

class

時,會把類加載請求委派給

BootStrapClassLoader

去完成;

第三:如果

BootStrapClassLoader

加載失敗(例如在

%JAVA_HOME%/jre/lib

裡未查找到該

class

),會使用

ExtClassLoader

來嘗試加載;

第四:如果

ExtClassLoader

也加載失敗,則會使用

AppClassLoader

來加載,如果

AppClassLoader

也加載失敗,則會報出異常

ClassNotFoundException

ClassLoader的雙親委派實作

ClassLoader

通過

loadClass()

方法實作了雙親委托機制,用于類的動态加載。

該方法的源碼如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }           

複制

loadClass方法本身是一個遞歸向上調用的過程,上述代碼中從parent.loadClass的調用就可以看出。

在執行其他操作之前,首先通過findLoadedClass方法從最底端的類加載器開始檢查是否已經加載指定的類。如果已經加載,則根據resolve參數決定是否要執行連接配接過程,并傳回

Class

對象。

而Jar包沖突往往發生在這裡,當第一個同名的類被加載之後,在這一步檢查時就會直接傳回,不會再加載真正需要的類。那麼,程式用到該類時就會抛出找不到類,或找不到類方法的異常。

Jar包的加載順序

上面已經看到一旦一個類被加載之後,全局限定名相同的類可能就無法被加載了。而Jar包被加載的順序直接決定了類加載的順序。

決定Jar包加載順序通常有以下因素:

  • 第一,Jar包所處的加載路徑。也就是加載該Jar包的類加載器在JVM類加載器樹結構中所處層級。上面講到的四類類加載器加載的Jar包的路徑是有不同的優先級的。
  • 第二,檔案系統的檔案加載順序。因Tomcat、Resin等容器的ClassLoader擷取加載路徑下的檔案清單時是不排序的,這就依賴于底層檔案系統傳回的順序,當不同環境之間的檔案系統不一緻時,就會出現有的環境沒問題,有的環境出現沖突。

本人遇到的問題屬于第二種因素中的一個分支情況,即同一目錄下不同Jar包的加載順序不同。是以,通過調整Jar包的加載順序就暫時解決了問題。

Jar包沖突的通常表現

Jar包沖突往往是很詭異的事情,也很難排查,但也會有一些共性的表現。

  • 抛出java.lang.ClassNotFoundException:典型異常,主要是依賴中沒有該類。導緻原因有兩方面:第一,的确沒有引入該類;第二,由于Jar包沖突,Maven仲裁機制選擇了錯誤的版本,導緻加載的Jar包中沒有該類。
  • 抛出java.lang.NoSuchMethodError:找不到特定的方法。Jar包沖突,導緻選擇了錯誤的依賴版本,該依賴版本中的類對不存在該方法,或該方法已經被更新。
  • 抛出java.lang.NoClassDefFoundError,java.lang.LinkageError等,原因同上。
  • 沒有異常但預期結果不同:加載了錯誤的版本,不同的版本底層實作不同,導緻預期結果不一緻。

Tomcat啟動時Jar包和類的加載順序

最後,梳理一下Tomcat啟動時,對Jar包和類的加載順序,其中包含上面提到的不同種類的類加載器預設加載的目錄:

  • $java_home/lib 目錄下的java核心api;
  • $java_home/lib/ext 目錄下的java擴充jar包;
  • java -classpath/-Djava.class.path所指的目錄下的類與jar包;
  • $CATALINA_HOME/common目錄下按照檔案夾的順序從上往下依次加載;
  • $CATALINA_HOME/server目錄下按照檔案夾的順序從上往下依次加載;
  • $CATALINA_BASE/shared目錄下按照檔案夾的順序從上往下依次加載;
  • 項目路徑/WEB-INF/classes下的class檔案;
  • 項目路徑/WEB-INF/lib下的jar檔案;

上述目錄中,同一檔案夾下的Jar包,按照順序從上到下一次加載。如果一個class檔案已經被加載到JVM中,後面相同的class檔案就不會被加載了。

小結

Jar包沖突在我們的日常開發中是非常常見的問題,如果能夠很好了解沖突的原因及底層機制,可以極大的提高解決問題的能力和團隊影響力。是以,在不少面試中都會被提及此類問題。

這篇文章我們重點講了手動添加依賴情況下導緻Jar包沖突的原因及解決方案。在解決該問題時往往還會設計到Maven對Jar包沖突管理的一些政策,比如依賴傳遞原則、最短路徑優先原則、最先聲明原則等,我們下篇文章再來詳細聊聊。

部落客簡介:《SpringBoot技術内幕》技術圖書作者,酷愛鑽研技術,寫技術幹貨文章。