你真的瞭解ClassLoader嗎?
這篇文章翻譯自zeroturnaround.com的 Do You Really Get Classloaders? ,融入和補充了筆者的一些實踐、經驗和樣例。本文的例子比原文更加具備實際意義,文字內容也更充沛一些,很是感謝做者 Jevgeni Kabanov 可以共享如此優秀的文檔。 java
1. 爲何你須要瞭解和敬畏ClassLoader
ClassLoader在Java語言中佔據了核心地位,Java應用服務器,OSGi,以及大量的網絡框架,它們大多數都用到了ClassLoader。若是在使用過程當中出現了類加載錯誤,你能解決它嗎? web
咱們將從JVM和開發者兩個角度講述ClassLoader,將會選擇一些典型的案例,而後演示如何解決它們。NoClassDefFoundError,LinkageError等不少錯誤都會有特定的表徵,咱們分析每一個例子,而後進行解決。 spring
2. 進入ClassLoader
每一個ClassLoader對象都是一個java.lang.ClassLoader的實例。每一個Class對象都被這些ClassLoader對象所加載,經過繼承java.lang.ClassLoader能夠擴展出自定義ClassLoader,並使用這些自定義的ClassLoader對類進行加載。 apache
先大致瞭解一下ClassLoader的API: bootstrap
package java.lang;
public abstract class ClassLoader {
public Class loadClass(String name);
protected Class defineClass(byte[] b);
public URL getResource(String name);
public Enumeration getResources(String name);
public ClassLoader getParent();
}
最重要的是ClassLoader的loadClass方法,它接受一個全類名,而後返回一個Class類型的實例。 api
defineClass方法接受一組字節,而後將其具體化爲一個Class類型實例,它通常從磁盤上加載一個文件,而後將文件的字節傳遞給JVM,經過JVM(native 方法)對於Class的定義,將其具體化,實例化爲一個Class類型實例。 瀏覽器
getParent方法返回其parent ClassLoader。 tomcat
getResource和getResources方法,從給定的repository中查找URLs,同時它們也具有相似loadClass同樣的代理機制,咱們能夠將loadClass視爲:defineClass(getResource(name).getBytes())。 服務器
Java因爲其晚綁定和「解釋型」的特性,類型的加載是到最晚才進行,一個類型直到被調用構造函數、靜態方法或者在字段上使用時纔會被加載。 websocket
考慮以下代碼:
2 |
public void doSomething() { |
代碼: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是沒有出錯提醒的,可是在運行時卻出現了錯誤。
看看以下示例:
02 |
* @author weipeng2k 2015年3月27日 下午5:15:15 |
04 |
@WebServlet(name ="NoClassDefFoundErrorServlet", urlPatterns ="/noClassDefFoundError.do") |
05 |
public class NoClassDefFoundErrorServletextends HttpServlet { |
07 |
private static final long serialVersionUID = 61585757018374721L; |
10 |
protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException { |
11 |
resp.getWriter().println(TestCase.class.toString()); |
在看pom.xml中對於依賴的定義:
03 |
<groupId>junit</groupId> |
04 |
<artifactId>junit</artifactId> |
05 |
<version>3.8.1</version> |
06 |
<scope>provided</scope> |
09 |
<groupId>javax.servlet</groupId> |
10 |
<artifactId>servlet-api</artifactId> |
11 |
<version>3.0</version> |
12 |
<scope>provided</scope> |
15 |
<groupId>org.springframework</groupId> |
16 |
<artifactId>spring</artifactId> |
17 |
<version>2.5.6</version> |
其中對於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; |
04 |
* @author weipeng2k 2015年3月31日 上午9:09:58 |
06 |
@WebServlet(name ="NoSuchMethodErrorServlet", urlPatterns = {"/noSuchMethodError.do" }) |
07 |
public class NoSuchMethodErrorServletextends HttpServlet { |
09 |
private static final long serialVersionUID = 1699609060417354821L; |
12 |
protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException { |
13 |
BeanFactoryUtils.isGeneratedBeanName("xxx"); |
15 |
resp.getWriter().println("done."); |
在doGet方法中調用了BeanFactoryUtils.isGeneratedBeanName(」xxx「);,看一下項目的pom依賴。
03 |
<groupId>junit</groupId> |
04 |
<artifactId>junit</artifactId> |
05 |
<version>4.11</version> |
06 |
<scope>provided</scope> |
09 |
<groupId>javax.servlet</groupId> |
10 |
<artifactId>servlet-api</artifactId> |
11 |
<version>3.0</version> |
12 |
<scope>provided</scope> |
15 |
<groupId>org.springframework</groupId> |
16 |
<artifactId>org.springframework.context</artifactId> |
17 |
<version>3.0.5.RELEASE</version> |
18 |
<scope>provided</scope> |
21 |
<groupId>org.apache.mina</groupId> |
22 |
<artifactId>mina-core</artifactId> |
23 |
<version>2.0.7</version> |
26 |
<groupId>com.alibaba.external</groupId> |
27 |
<artifactId>sourceforge.spring</artifactId> |
28 |
<version>2.0.7</version> |
這裏爲了方便觀察到結果,將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; |
04 |
import java.io.IOException; |
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; |
13 |
import org.apache.mina.proxy.utils.MD4; |
15 |
import com.murdock.classloader.CachedClassLoader; |
18 |
* @author weipeng2k 2015年4月4日 下午6:00:54 |
20 |
@WebServlet(name ="ClassCastExceptionServlet", urlPatterns ="/classCastException.do") |
21 |
public class ClassCastExceptionServletextends HttpServlet { |
22 |
private static final long serialVersionUID = -8959000121057369987L; |
25 |
protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException { |
26 |
String localFirst = req.getParameter("localFirst"); |
27 |
CachedClassLoader cl =null; |
28 |
cl =new CachedClassLoader( |
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); |
36 |
Class<?> klass = cl.loadClass("org.apache.mina.proxy.utils.MD4"); |
37 |
MD4 md4 = (MD4) klass.newInstance(); |
39 |
resp.getWriter().println(md4); |
41 |
}catch (Exception ex) { |
42 |
throw new RuntimeException(ex); |
在ClassCastExceptionServlet中,構建了一個CachedClassLoader,利用這個ClassLoader加載org.apache.mina.proxy.utils.MD4,而後反射調用構造該類的實例,將其賦給MD4,最後將其打印到瀏覽器。
請求URL:http://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邏輯:
03 |
clazz = findClass(name); |
07 |
}catch (ClassNotFoundException ex) { |
10 |
return super.loadClass(name); |
12 |
return super.loadClass(name); |
能夠看到在 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;"theclass 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 objectsfor 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,表示解析的語句所在的類型Param2是LinkageErrorTest$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中被覆蓋的方法返回的類型Param2爲Launcher$AppClassLoader加載。
Linkage在常規狀況下很是難以製造,只有在多個類加載器交互時纔有可能出現,下面看一下問題代碼。出現問題的類和參數:
01 |
package com.murdock.classloader.linkageerror; |
04 |
* @author weipeng2k 2015年4月28日 上午10:04:26 |
06 |
public class HandleUtils { |
07 |
public void m(Param param) { |
13 |
package com.murdock.classloader.linkageerror; |
16 |
public Param2 generate() { |
21 |
package com.murdock.classloader.linkageerror; |
23 |
public class Param2extends Param { |
24 |
public Param2 generate() { |
測試用例以下:
02 |
public void test()throws Exception { |
04 |
// cl1在加載HandleUtils和Param時將會使用AppClassLoader |
05 |
URLClassLoader cl1 =new URLClassLoader(new URL[] {newFile("target/test-classes").toURI().toURL()},null) { |
08 |
public Class<?> loadClass(String name)throwsClassNotFoundException { |
09 |
if("com.murdock.classloader.linkageerror.HandleUtils".equals(name)) { |
10 |
return ClassLoader.getSystemClassLoader().loadClass(name); |
13 |
if ("com.murdock.classloader.linkageerror.Param".equals(name)) { |
14 |
return ClassLoader.getSystemClassLoader().loadClass(name); |
17 |
return super.loadClass(name); |
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()); |
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是否存在,若是可以找到則會輸出到終端,這裏就找到了ArchiveUtils和ArrayUtils兩個符合要求的類。若是沒法找到,那麼就多是pom.xml的依賴配置不正確了,須要檢查一下。
10. 使用Middleware-Detector進行檢查類衝突
出現了 NoSuchMethodError 或者 NoSuchFieldError ,這時通常是應用的classpath下包含了多個包含了想同類的jar包,而很不幸的加載到了 不正確 的jar包。
咱們能夠經過使用Middleware-Detector的類查找進行定位,可是不能發現一個修復一個,這裏Middleware-Detector提供了一個檢查classpath下有衝突jar包的功能。只須要設置classpath的目錄,而後運行cc –check tomcat#1便可。有衝突的jar就須要本身在pom.xml裏面進行仲裁或者排除了。