深入淺出ClassLoader

你真的瞭解ClassLoader嗎?

這篇文章翻譯自zeroturnaround.com的 Do You Really Get Classloaders? ,融入和補充了筆者的一些實踐、經驗和樣例。本文的例子比原文更加具有實際意義,文字內容也更充沛一些,非常感謝作者 Jevgeni Kabanov 能夠共享如此優秀的文檔。

 

1. 爲什麼你需要了解和敬畏ClassLoader

ClassLoader在Java語言中佔據了核心地位,Java應用服務器,OSGi,以及大量的網絡框架,它們大多數都用到了ClassLoader。如果在使用過程中出現了類加載錯誤,你能解決它嗎?

我們將從JVM和開發者兩個角度講述ClassLoader,將會選擇一些典型的案例,然後演示如何解決它們。NoClassDefFoundError,LinkageError等很多錯誤都會有特定的表徵,我們分析每個例子,然後進行解決。

2. 進入ClassLoader

每個ClassLoader對象都是一個java.lang.ClassLoader的實例。每個Class對象都被這些ClassLoader對象所加載,通過繼承java.lang.ClassLoader可以擴展出自定義ClassLoader,並使用這些自定義的ClassLoader對類進行加載。

先大體瞭解一下ClassLoader的API:

01 package java.lang;
02  
03 public abstract class ClassLoader {
04  public Class loadClass(String name);
05  
06  protected Class defineClass(byte[] b);
07  
08  public URL getResource(String name);
09  
10  public Enumeration getResources(String name);
11  
12  public ClassLoader getParent();
13 }

    最重要的是ClassLoaderloadClass方法,它接受一個全類名,然後返回一個Class類型的實例。

    defineClass方法接受一組字節,然後將其具體化爲一個Class類型實例,它一般從磁盤上加載一個文件,然後將文件的字節傳遞給JVM,通過JVMnative 方法)對於Class的定義,將其具體化,實例化爲一個Class類型實例。

    getParent方法返回其parent ClassLoader

    getResourcegetResources方法,從給定的repository中查找URLs,同時它們也具備類似loadClass一樣的代理機制,我們可以將loadClass視爲:defineClass(getResource(name).getBytes())

    Java由於其晚綁定和解釋型的特性,類型的加載是到最晚才進行,一個類型直到被調用構造函數、靜態方法或者在字段上使用時纔會被加載。

    考慮如下代碼:

1 public class A {
2      public void doSomething() {
3           B b = new B();
4           b.doSomethingElse();
5      }
6 }

代碼:B b = new B();等同於B b = Class.forName(“B”, false, A.class.getClassLoader()).newInstance();

這代表着,在類型A中使用到的類型,將由加載了類型A的類加載器來進行加載。

3. ClassLoader繼承體系

當啓動一個JVM時,bootstrap 類加載器就會加載java的核心類,例如:rt.jar中的類。bootstrap 類加載器是其他類加載器的parent,它使唯一一個沒有parent的類加載器。

接下來是extension 類加載器,它以bootstrap 類加載器作爲parent,它用來從Java系統變量java.ext.dir中的jar包中加載類的。

第三個,也是最重要的一個就是開發者使用的system classpath 類加載器 。它是extension 類加載器 的child,它用來從Java系統變量java.class.path下面加載類,可以通過 -classpath 來指定這個位置。

注意類加載器的體系並不是“繼承”體系,而是一個“委派”體系。大多數類加載器首先會到自己的parent中查找類或者資源,如果找不到,纔會在自己的本地進行查找。事實上,類加載器被定義加載哪些在parent中無法加載到的類,這樣在較高層級的類加載器上的類型能夠被“賦值”爲較低類加載器加載的類型。

類加載器的委託行爲動機是爲了避免相同的類被加載多次。回到1995年,Java的主要方向被放在Applet上,那時候網絡帶寬優先,所以程序中的類直到用時纔會被加載。但是事實上,Java在服務器端展示了強勁的能力,但是服務器端要求類加載器能夠反轉委派原則,也就是先加載本地的類,如果加載不到,再到parent中加載。

JavaEE的 委派模型

    每個方塊都是一個類加載器,JavaEE規範推薦每個模塊的類加載器先加載本類加載的內容,如果加載不到纔回到parent類加載器中嘗試加載。

    反轉委派原則的原因是應用服務器中所攜帶的類庫並不是應用所期待的,也許不適合應用開發者,一個常見的例子就是log4j的依賴在容器和不同的應用中都存在,但是它們的版本大都不同。

    Tomcat 類加載順序(開啓了delegate模式)

    在Tomcat中,默認的行爲是先嚐試在Bootstrap和Extension中進行類型加載,如果加載不到則在WebappClassLoader中進行加載,如果還是找不到則在Common中進行查找。在Alibaba使用的Tomcat開啓了delegate模式,因此加載類型時會以parent類加載器優先。

4. NoClassDefFoundError

NoClassDefFoundError是在開發JavaEE程序中常見的一種問題。該問題會隨着你所使用的JavaEE中間件環境的複雜度以及應用本身的體量變得更加複雜,尤其是現在的JavaEE服務器具有大量的類加載器。

在JavaDoc中對NoClassDefFoundError的產生是由於JVM或者類加載器實例嘗試加載類型的定義,但是該定義卻沒有找到,影響了執行路徑。換句話說,在編譯時這個類是能夠被找到的,但是在執行時卻沒有找到。

這一刻IDE是沒有出錯提醒的,但是在運行時卻出現了錯誤。

看看如下示例:

01 /**
02  * @author weipeng2k 2015年3月27日 下午5:15:15
03  */
04 @WebServlet(name = "NoClassDefFoundErrorServlet", urlPatterns ="/noClassDefFoundError.do")
05 public class NoClassDefFoundErrorServlet extends HttpServlet {
06  
07     private static final long serialVersionUID = 61585757018374721L;
08  
09     @Override
10     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throwsServletException, IOException {
11         resp.getWriter().println(TestCase.class.toString());
12     }
13 }

    在看pom.xml中對於依賴的定義:

 

01 <dependencies>
02     <dependency>
03         <groupId>junit</groupId>
04         <artifactId>junit</artifactId>
05         <version>3.8.1</version>
06         <scope>provided</scope>
07     </dependency>
08     <dependency>
09         <groupId>javax.servlet</groupId>
10         <artifactId>servlet-api</artifactId>
11         <version>3.0</version>
12         <scope>provided</scope>
13     </dependency>
14     <dependency>
15         <groupId>org.springframework</groupId>
16         <artifactId>spring</artifactId>
17         <version>2.5.6</version>
18     </dependency>
19 </dependencies>

其中對於junit的依賴是provided級別的,這裏是爲了能簡化錯誤出現的條件。可以看到,在NoClassDefFoundErrorServlet中,使用了junit.jar中的TestCase,但是junit.jar在WEB-INF/lib中卻沒有,從而導致WebappClassLoader在進行加載TestCase時無法找到,從而拋出NoClassDefFoundError。我們需要從最終的war包中確定是否存在這個類,而不是在IDE中進行搜索。

5. NoSuchMethodError

在另一個場景中,我們可能遇到了另一個錯誤,也就是NoSuchMethodError。

NoSuchMethodError代表這個類型確實存在,但是一個不正確的版本被加載了。爲了解決這個問題我們可以使用 ‘-verbose:class’ 來判斷該JVM加載的到底是哪個版本。

看如下示例:

01 import org.springframework.beans.factory.BeanFactoryUtils;
02  
03 /**
04  * @author weipeng2k 2015年3月31日 上午9:09:58
05  */
06 @WebServlet(name = "NoSuchMethodErrorServlet", urlPatterns = { "/noSuchMethodError.do"})
07 public class NoSuchMethodErrorServlet extends HttpServlet {
08  
09     private static final long serialVersionUID = 1699609060417354821L;
10  
11     @Override
12     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throwsServletException, IOException {
13         BeanFactoryUtils.isGeneratedBeanName("xxx");
14  
15         resp.getWriter().println("done.");
16     }
17 }

在doGet方法中調用了BeanFactoryUtils.isGeneratedBeanName(”xxx“);,看一下項目的pom依賴。

01 <dependencies>
02     <dependency>
03         <groupId>junit</groupId>
04         <artifactId>junit</artifactId>
05         <version>4.11</version>
06         <scope>provided</scope>
07     </dependency>
08     <dependency>
09         <groupId>javax.servlet</groupId>
10         <artifactId>servlet-api</artifactId>
11         <version>3.0</version>
12         <scope>provided</scope>
13     </dependency>
14     <dependency>
15         <groupId>org.springframework</groupId>
16         <artifactId>org.springframework.context</artifactId>
17         <version>3.0.5.RELEASE</version>
18         <scope>provided</scope>
19     </dependency>
20     <dependency>
21         <groupId>org.apache.mina</groupId>
22         <artifactId>mina-core</artifactId>
23         <version>2.0.7</version>
24     </dependency>
25     <dependency>
26         <groupId>com.alibaba.external</groupId>
27         <artifactId>sourceforge.spring</artifactId>
28         <version>2.0.7</version>
29     </dependency>
30 </dependencies>

這裏爲了方便觀察到結果,將org.springframework.context的 scope 改爲了 provided ,目的是不將其打包入war包,而只是使用了sourceforge.spring中定義的2.0.7版本,這個版本肯定沒有isGeneratedBeanName(String name)方法,但是在IDE中,由於應用依賴到了高版本的spring從而能夠編譯通過,但是在運行時卻沒有那麼好運了。這種錯誤,常見於 Maven座標 的變動,使得應用依賴了多個 相同內容,不同版本 的jar包,以致在運行時選擇了非期望的版本。

6. ClassCastException

NoClassDefFoundError和NoSuchMethodError是兩個在 JavaEE 環境中經常出現的問題,這些問題需要 開發人員瞭解問題的本質,才能夠被 從容 的處理。

下面我們看一下ClassCastException,在一個類加載器的情況下,一般出現這種錯誤都會是在轉型操作時,比如:A a = (A) method();,很容易判斷出來method()方法返回的類型不是類型A,但是在 JavaEE 多個類加載器的環境下就會出現一些難以定位的情況。

看如下示例:

01 package com.murdock.classloader.servlet;
02  
03 import java.io.File;
04 import java.io.IOException;
05 import java.net.URL;
06  
07 import javax.servlet.ServletException;
08 import javax.servlet.annotation.WebServlet;
09 import javax.servlet.http.HttpServlet;
10 import javax.servlet.http.HttpServletRequest;
11 import javax.servlet.http.HttpServletResponse;
12  
13 import org.apache.mina.proxy.utils.MD4;
14  
15 import com.murdock.classloader.CachedClassLoader;
16  
17 /**
18  * @author weipeng2k 2015年4月4日 下午6:00:54
19  */
20 @WebServlet(name = "ClassCastExceptionServlet", urlPatterns = "/classCastException.do")
21 public class ClassCastExceptionServlet extends HttpServlet {
22     private static final long   serialVersionUID    = -8959000121057369987L;
23  
24     @Override
25     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throwsServletException, IOException {
26         String localFirst = req.getParameter("localFirst");
27         CachedClassLoader cl = null;
28         cl = new CachedClassLoader(
29                 new URL[] { new File(
30                         "/Users/weipeng2k/.m2/repository/org/apache/mina/mina-core/2.0.7/mina-core-2.0.7.jar").toURI()
31                         .toURL() }, this.getClass().getClassLoader());
32         if ("false".equals(localFirst)) {
33             cl.setLocalFirst(false);
34         }
35         try {
36             Class<?> klass = cl.loadClass("org.apache.mina.proxy.utils.MD4");
37             MD4 md4 = (MD4) klass.newInstance();
38  
39             resp.getWriter().println(md4);
40  
41         catch (Exception ex) {
42             throw new RuntimeException(ex);
43         finally {
44             cl.close();
45         }
46  
47     }
48 }

在ClassCastExceptionServlet中,構建了一個CachedClassLoader,利用這個ClassLoader加載org.apache.mina.proxy.utils.MD4,然後反射調用構造該類的實例,將其賦給MD4,最後將其打印到瀏覽器。

請求URLhttp://localhost:8080/classCastException.do

響應頁面,出現錯誤:

1 java.lang.RuntimeException: java.lang.ClassCastException: org.apache.mina.proxy.utils.MD4 cannot be cast to org.apache.mina.proxy.utils.MD4
2     com.murdock.classloader.servlet.ClassCastExceptionServlet.doGet(ClassCastExceptionServlet.java:42)
3     javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
4     javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
5     org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)

請求URL :http://localhost:8080/classCastException.do?localFirst=false
響應頁面,輸出正常:

1 org.apache.mina.proxy.utils.MD4@401c8af5

    請求的URL加上了localFirst=false就可以正常的輸出,而它也就是在CachedClassLoder上設置了一下,爲什麼有這麼大的差別。org.apache.mina.proxy.utils.MD4全類名一致,爲什麼會出現ClassCastException呢?

    JVM中,如何確定一個類型實例?答:全類名嗎?不是,是類加載器加上全類名。在JVM中,類型被定義在一個叫SystemDictionary 的數據結構中,該數據結構接受類加載器和全類名作爲參數,返回類型實例。

     SystemDictionary 如圖所示:

    類型加載時,需要傳入類加載器和需要加載的全類名,如果在 SystemDictionary 中能夠命中一條記錄,則返回class 列上對應的類型實例引用,如果無法命中記錄,則會調用loader.loadClass(name);進行類型加載。

    這裏不會更加深入的介紹 SystemDictionary 如何進行類型加載的過程,而是需要指出 JVM中確定一個類型的座標是通過類加載器和全類名做到的 。回想一下MD4 md4 = (MD4) klass.newInstance();,是不是代表着等式兩邊的MD4是不同的類加載器加載的呢?那問題一定出在 CachedClassLoader 上。這裏貼一下loadClass(String name)方法的部分邏輯。

     CachedClassLoader loadClass邏輯:

01 if (localFirst) {
02     try {
03         clazz = findClass(name);
04         if (clazz != null) {
05             return clazz;
06         }
07     catch (ClassNotFoundException ex) {
08  
09     }
10     return super.loadClass(name);
11 else {
12     return super.loadClass(name);
13 }

可以看到在 localFirst 爲true時,該類加載器會首先加載自身 repository 中的類型,如果加載不到,則會嘗試默認的加載機制進行加載,也就是parent優先加載。這樣就可以解釋MD4 md4 = (MD4) klass.newInstance();,等式左邊MD4 md4,這個類型是WebappClassLoader.org.apache.mina.proxy.utils.MD4,等式右邊klass.newInstance()返回的類型是CachedClassLoader.org.apache.mina.proxy.utils.MD4,二者並不是同一個類型,所以無法完成類型轉換,最終拋出 ClassCastException 。而當 localFirst 爲false時,該類加載器遵循parent優先,從而會先委派給WebappClassLoader進行加載,當然轉型也就不會有問題了。

在傳統的雙親委派模型下,這種 ClassCastException 是不會發生的,因爲它的加載順序杜絕了出現這種問題的可能,而在 JavaEE 環境下,每個資源模塊(比如一個war包)都優先使用自身的資源,正因爲突破了雙親委派模型, 奇怪的問題 就發生了。

7. LinkageError

有時候事情會變得更糟,和 ClassCastException 本質一樣,加載自不同位置的*相同類*在同一段邏輯(比如:方法)中交互時,會出現 LinkageError 。

我們先看一下出錯的異常信息,然後分析一下它產生的條件和原因:

01 java.lang.LinkageError: loader constraint violation: when resolving overridden method"com.murdock.classloader.linkageerror.Param2.generate()Lcom/murdock/classloader/linkageerror/Param2;"the class loader (instance of com/murdock/classloader/linkageerror/LinkageErrorTest$1) of the currentclass, com/murdock/classloader/linkageerror/Param2, and its superclass loader (instance of sun/misc/Launcher$AppClassLoader), have different Class objects for the type com/murdock/classloader/linkageerror/Param2 used in the signature
02     at java.lang.Class.getDeclaredConstructors0(Native Method)
03     at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
04     at java.lang.Class.getConstructor0(Class.java:3075)
05     at java.lang.Class.newInstance(Class.java:412)
06     at com.murdock.classloader.linkageerror.LinkageErrorTest.test(LinkageErrorTest.java:34)
07     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
08     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
09     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
10     at java.lang.reflect.Method.invoke(Method.java:497)
11     at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
12     at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
13     at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
14     at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)

    看到一堆出錯信息,但是不要緊張,慢慢的讀一下出錯信息,這種錯誤一般會讓你直覺感覺不會出現。loader constraint violation表示類加載器衝突了,這句話暗示: 相同的類,由不同的ClassLoader加載,但是在這裏遇到了when resolving overridden method "com.murdock.classloader.linkageerror.Param2.generate()Lcom/murdock/classloader/linkageerror/Param2;"表示在解析那條語句出現了問題,這裏表示在Param2.generate()方法的解析過程中出現了問題。the class loader (instance of com/murdock/classloader/linkageerror/LinkageErrorTest$1) of the current class, com/murdock/classloader/linkageerror/Param2,表示解析的語句所在的類型Param2LinkageErrorTest$1類加載器加載的。and its superclass loader (instance of sun/misc/Launcher$AppClassLoader), have different Class objects for the type com/murdock/classloader/linkageerror/Param2 used in the signature表示Param2的超類Param中被覆蓋的方法返回的類型Param2Launcher$AppClassLoader加載。

    Linkage在常規情況下非常難以製造,只有在多個類加載器交互時纔有可能出現,下面看一下問題代碼。出現問題的類和參數:

01 package com.murdock.classloader.linkageerror;
02  
03 /**
04  * @author weipeng2k 2015年4月28日 上午10:04:26
05  */
06 public class HandleUtils {
07     public void m(Param param) {
08         param.generate();
09     }
10  
11 }
12  
13 package com.murdock.classloader.linkageerror;
14  
15 public class Param {
16     public Param2 generate() {
17         return new Param2();
18     }
19 }
20  
21 package com.murdock.classloader.linkageerror;
22  
23 public class Param2 extends Param {
24     public Param2 generate() {
25         return new Param2();
26     }
27 }

    測試用例如下:

01 @Test
02 public void test() throws Exception {
03  
04     // cl1在加載HandleUtils和Param時將會使用AppClassLoader
05     URLClassLoader cl1 = new URLClassLoader(new URL[] {new File("target/test-classes").toURI().toURL()}, null) {
06  
07         @Override
08         public Class<?> loadClass(String name) throws ClassNotFoundException {
09             if ("com.murdock.classloader.linkageerror.HandleUtils".equals(name)) {
10                 return ClassLoader.getSystemClassLoader().loadClass(name);
11             }
12  
13             if ("com.murdock.classloader.linkageerror.Param".equals(name)) {
14                 return ClassLoader.getSystemClassLoader().loadClass(name);
15             }
16  
17             return super.loadClass(name);
18         }
19  
20     };
21  
22     ClassLoader.getSystemClassLoader().loadClass("com.murdock.classloader.linkageerror.Param2");
23     HandleUtils hu = (HandleUtils) cl1.loadClass("com.murdock.classloader.linkageerror.HandleUtils").newInstance();
24     hu.m((Param) cl1.loadClass("com.murdock.classloader.linkageerror.Param2").newInstance());
25 }

LinkageError 需要觀察哪個類被不同的類加載器加載了,在哪個方法或者調用處發生(交匯)的,然後才能想解決方法,解決方法無外乎兩種。第一,還是不同的類加載器加載,但是相互不再交匯影響,這裏需要針對發生問題的地方做一些改動,比如更換實現方式,避免出現上述問題;第二,衝突的類需要由一個Parent類加載器進行加載。**LinkageError** 和**ClassCastException** 本質是一樣的,加載自不同類加載器的類型,在同一個類的方法或者調用中出現,如果有轉型操作那麼就會拋 ClassCastException ,如果是直接的方法調用處的參數或者返回值解析,那麼就會產生 LinkageError 。

8. 類加載器問題對照表

遇到類加載器問題時,可以嘗試使用下面的表格進行問題排查。

類找不到 了不正確的類 多於一個類被加
ClassNotFoundException NoClassDefFoundError IncompatibleClassChangeError NoSuchMethodError NoSuchFieldError IllegalAccessError ClassCastException LinkageError
IDE class lookup (Ctrl+Shift+T in Eclipse)find . -name “*.jar” -exec jar -tf {} \; | grep DateUtils

 

使用middleware-detector

通過在啓動參數中加 -verbose:class,觀察加載的類來自哪個jar包使用middelware-detector 通過`-verbose:class`觀察

9. 使用Middleware-Detector進行類查找

出現了 ClassNotFoundException 或者 NoClassDefFoundError ,需要檢查一下程序的classpath下面是否存在你所預想的類。這時可以使用Middleware-Detector工具進行類查找,該工具是Alibaba中間件團隊開發的一款中間件問題診斷工具,當然也包括了許多支持性質的工具。

下面我們使用Middleware-Detector進行類查找,比如我們要查找apache的Utils,我們懷疑這個類在classpath下找不到。

啓動middleware-detector,查看 Pandora 提供的自定義檢查器,目前編號爲1的Pandora自定義檢查器就是進行classpath下的指定類或者接口的查找工作。

    配置classpath目錄以及需要查找的類名,這裏類名支持 * 號進行模糊匹配。可以看到設定當前的classpath目錄到了WEB-INF/lib 下面,然後找尋*apache*comm*A*Utils是否存在,如果能夠找到則會輸出到終端,這裏就找到了ArchiveUtilsArrayUtils兩個符合要求的類。如果無法找到,那麼就可能是pom.xml的依賴配置不正確了,需要檢查一下。

10. 使用Middleware-Detector進行檢查類衝突

出現了 NoSuchMethodError 或者 NoSuchFieldError ,這時一般是應用的classpath下包含了多個包含了想同類的jar包,而很不幸的加載到了 不正確 的jar包。

我們可以通過使用Middleware-Detector的類查找進行定位,但是不能發現一個修復一個,這裏Middleware-Detector提供了一個檢查classpath下有衝突jar包的功能。只需要設置classpath的目錄,然後運行cc –check tomcat#1即可。有衝突的jar就需要自己在pom.xml裏面進行仲裁或者排除了。

發佈了36 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章