深入浅出ClassLoader

来源:互联网 发布:1998鹿鼎记 gotv源码 编辑:程序博客网 时间:2024/05/20 06:08


1. 为什么你需要了解和敬畏ClassLoader

ClassLoader在Java语言中占据了核心地位,Java应用服务器,OSGi,以及大量的网络框架,它们大多数都用到了ClassLoader。如果在使用过程中出现了类加载错误,你能解决它吗?

我们将从JVM和开发者两个角度讲述ClassLoader,将会选择一些典型的案例,然后演示如何解决它们。NoClassDefFoundError,LinkageError等很多错误都会有特定的表征,我们分析每个例子,然后进行解决。

ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。 

1.1 几个相关概念ClassLoader负责载入系统的所有Resources(Class,文件,来自网络的字节流等),通过ClassLoader从而将资源载入JVM  
每个class都有一个reference,指向自己的ClassLoader。Class.getClassLoader()  
array的ClassLoader就是其元素的ClassLoader,若是基本数据类型,则这个array没有ClassLoader  
1.2 主要方法和工作过程Java1.1及从前版本中,ClassLoader主要方法:  
Class loadClass( String name, boolean resolve ); ClassLoader.loadClass() 是 ClassLoader 的入口点  
defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。  
findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。  
resolveClass可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值  
findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法  
一般load方法过程如下:  

调用 findLoadedClass 来查看是否存在已装入的类。  
如果没有,那么采用某种特殊的神奇方式来获取原始字节。(通过IO从文件系统,来自网络的字节流等)  
如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。  
如果没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。  
如果 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。  
如果还没有类,返回 ClassNotFoundException。  
否则,将类返回给调用程序。  
1.3 委托模型自从JDK1.2以后,ClassLoader做了改进,使用了委托模型,所有系统中的ClassLoader组成一棵树,ClassLoader在载入类库时先让Parent寻找,Parent找不到才自己找。  
JVM在运行时会产生三个ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。其中,Bootstrap ClassLoader是用C++编写的,在Java中看不到它,是null。它用来加载核心类库,就是在lib下的类库,Extension ClassLoader加载lib/ext下的类库,App ClassLoader加载Classpath里的类库,三者的关系为:App ClassLoader的Parent是Extension ClassLoader,而Extension ClassLoader的Parent为Bootstrap ClassLoader。加载一个类时,首先BootStrap进行寻找,找不到再由Extension ClassLoader寻找,最后才是App ClassLoader。  

将ClassLoader设计成委托模型的一个重要原因是出于安全考虑,比如在Applet中,如果编写了一个java.lang.String类并具有破坏性。假如不采用这种委托机制,就会将这个具有破坏性的String加载到了用户机器上,导致破坏用户安全。但采用这种委托机制则不会出现这种情况。因为要加载java.lang.String类时,系统最终会由Bootstrap进行加载,这个具有破坏性的String永远没有机会加载。  

委托模型还带来了一些问题,在某些情况下会产生混淆,如下是Tomcat的ClassLoader结构图:  

                Bootstrap 
                  | 
                System 
                  | 
                Common 
                /     
            Catalina  Shared 
                      /     
                   Webapp1  Webapp2 ... 

由 Common 类装入器装入的类决不能(根据名称)直接访问由 Web 应用程序装入的类。使这些类联系在一起的唯一方法是通过使用这两个类集都可见的接口。在这个例子中,就是包含由 Java servlet 实现的 javax.servlet.Servlet。  
如果在lib或者lib/ext等类库有与应用中同样的类,那么应用中的类将无法被载入。通常在jdk新版本出现有类库移动时会出现问题,例如最初我们使用自己的xml解析器,而在jdk1.4中xml解析器变成标准类库,load的优先级也高于我们自己的xml解析器,我们自己的xml解析器永远无法找到,将可能导致我们的应用无法运行。  

相同的类,不同的ClassLoader,将导致ClassCastException异常  

1.4 线程中的ClassLoader每个运行中的线程都有一个成员contextClassLoader,用来在运行时动态地载入其它类,可以使用方法Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为;也可以通过方法Thread.currentThread().getContextClassLoader()来获得当前线程的ClassLoader。  
实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个  

this.getClass.getClassLoader();  
Thread.currentThread().getContextClassLoader();  
方法一得到的Classloader是静态的,表明类的载入者是谁;方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。  

1.5 Web应用中的ClassLoader回到上面的例子,在Tomcat里,WebApp的ClassLoader的工作原理有点不同,它先试图自己载入类(在ContextPath/WEB-INF/...中载入类),如果无法载入,再请求父ClassLoader完成。  
由此可得:  

对于WEB APP线程,它的contextClassLoader是WebAppClassLoader  
对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader  
1.6 获得ClassLoader的几种方法可以通过如下3种方法得到ClassLoader  
this.getClass.getClassLoader(); // 使用当前类的ClassLoader  
Thread.currentThread().getContextClassLoader(); // 使用当前线程的ClassLoader  
ClassLoader.getSystemClassLoader(); // 使用系统ClassLoader,即系统的入口点所使用的ClassLoader。(注意,system ClassLoader与根ClassLoader并不一样。JVM下system ClassLoader通常为App ClassLoader)  
1.7 几种扩展应用用户定制自己的ClassLoader可以实现以下的一些应用  
安全性。类进入JVM之前先经过ClassLoader,所以可以在这边检查是否有正确的数字签名等  
加密。java字节码很容易被反编译,通过定制ClassLoader使得字节码先加密防止别人下载后反编译,这里的ClassLoader相当于一个动态的解码器  
归档。可能为了节省网络资源,对自己的代码做一些特殊的归档,然后用定制的ClassLoader来解档  
自展开程序。把java应用程序编译成单个可执行类文件,这个文件包含压缩的和加密的类文件数据,同时有一个固定的ClassLoader,当程序运行时它在内存中完全自行解开,无需先安装  
动态生成。可以生成应用其他还未生成类的类,实时创建整个类并可在任何时刻引入JVM  
2.0 资源载入 
所有资源都通过ClassLoader载入到JVM里,那么在载入资源时当然可以使用ClassLoader,只是对于不同的资源还可以使用一些别的方式载入,例如对于类可以直接new,对于文件可以直接做IO等。 

2.1 载入类的几种方法假设有类A和类B,A在方法amethod里需要实例化B,可能的方法有3种。对于载入类的情况,用户需要知道B类的完整名字(包括包名,例如"com.rain.B")  
1. 使用Class静态方法 Class.forName  

    Class cls = Class.forName("com.rain.B"); 
    B b = (B)cls.newInstance(); 

2. 使用ClassLoader  
    /* Step 1. Get ClassLoader */ 
    ClassLoader cl; // 如何获得ClassLoader参考1.6 

    /* Step 2. Load the class */ 
    Class cls = cl.loadClass("com.rain.B"); // 使用第一步得到的ClassLoader来载入B 
     
    /* Step 3. new instance */ 
    B b = (B)cls.newInstance(); // 有B的类得到一个B的实例 

3. 直接new  
    B b = new B(); 

2.2 文件载入(例如配置文件等)假设在com.rain.A类里想读取文件夹 /com/rain/config 里的文件sys.properties,读取文件可以通过绝对路径或相对路径,绝对路径很简单,在Windows下以盘号开始,在Unix下以"/"开始  
对于相对路径,其相对值是相对于ClassLoader的,因为ClassLoader是一棵树,所以这个相对路径和ClassLoader树上的任何一个ClassLoader相对比较后可以找到文件,那么文件就可以找到,当然,读取文件也使用委托模型  

1. 直接IO  

/** 
 * 假设当前位置是 "C:/test",通过执行如下命令来运行A "java com.rain.A" 
 * 1. 在程序里可以使用绝对路径,Windows下的绝对路径以盘号开始,Unix下以"/"开始 
 * 2. 也可以使用相对路径,相对路径前面没有"/" 
 * 因为我们在 "C:/test" 目录下执行程序,程序入口点是"C:/test",相对路径就 
 * 是 "com/rain/config/sys.properties" 
 * (例子中,当前程序的ClassLoader是App ClassLoader,system ClassLoader = 当前的 
 * 程序的ClassLoader,入口点是"C:/test") 
 * 对于ClassLoader树,如果文件在jdk lib下,如果文件在jdk lib/ext下,如果文件在环境变量里, 
 * 都可以通过相对路径"sys.properties"找到,lib下的文件最先被找到 
 */ 
File f = new File("C:/test/com/rain/config/sys.properties"); // 使用绝对路径 
//File f = new File("com/rain/config/sys.properties"); // 使用相对路径 
InputStream is = new FileInputStream(f); 

如果是配置文件,可以通过java.util.Properties.load(is)将内容读到Properties里,Properties默认认为is的编码是ISO-8859-1,如果配置文件是非英文的,可能出现乱码问题。  
2. 使用ClassLoader  

/** 
 * 因为有3种方法得到ClassLoader,对应有如下3种方法读取文件 
 * 使用的路径是相对于这个ClassLoader的那个点的相对路径,此处只能使用相对路径 
 */ 
InputStream is = null; 
is = this.getClass().getClassLoader().getResourceAsStream( 
       "com/rain/config/sys.properties"); //方法1 
//is = Thread.currentThread().getContextClassLoader().getResourceAsStream( 
       "com/rain/config/sys.properties"); //方法2 
//is = ClassLoader.getSystemResourceAsStream("com/rain/config/sys.properties"); //方法3 

如果是配置文件,可以通过java.util.Properties.load(is)将内容读到Properties里,这里要注意编码问题。  
3. 使用ResourceBundle  

    ResourceBundle bundle = ResourceBundle.getBoundle("com.rain.config.sys"); 

这种用法通常用来载入用户的配置文件,关于ResourceBunlde更详细的用法请参考其他文档  
总结:有如下3种途径来载入文件  

    1. 绝对路径 ---> IO 
    2. 相对路径 ---> IO 
                ---> ClassLoader 
    3. 资源文件 ---> ResourceBundle 

2.3 如何在web应用里载入资源在web应用里当然也可以使用ClassLoader来载入资源,但更常用的情况是使用ServletContext,如下是web目录结构  
    ContextRoot 
       |- JSP、HTML、Image等各种文件 
        |- [WEB-INF] 
              |- web.xml 
              |- [lib] Web用到的JAR文件 
                |- [classes] 类文件 

用户程序通常在classes目录下,如果想读取classes目录里的文件,可以使用ClassLoader,如果想读取其他的文件,一般使用ServletContext.getResource()  
如果使用ServletContext.getResource(path)方法,路径必须以"/"开始,路径被解释成相对于ContextRoot的路径,此处载入文件的方法和ClassLoader不同,举例"/WEB-INF/web.xml","/download/WebExAgent.rar"

2. 进入ClassLoader

每个ClassLoader对象都是一个java.lang.ClassLoader的实例。每个Class对象都被这些ClassLoader对象所加载,通过继承java.lang.ClassLoader可以扩展出自定义ClassLoader,并使用这些自定义的ClassLoader对类进行加载。

先大体了解一下ClassLoader的API:

01package java.lang;
02 
03public 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由于其晚绑定和解释型的特性,类型的加载是到最晚才进行,一个类型直到被调用构造函数、静态方法或者在字段上使用时才会被加载。

    考虑如下代码:

1public 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")
05public class NoClassDefFoundErrorServlet extends HttpServlet {
06 
07    private static final long serialVersionUID = 61585757018374721L;
08 
09    @Override
10    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, 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加载的到底是哪个版本。

看如下示例:

01import org.springframework.beans.factory.BeanFactoryUtils;
02 
03/**
04 * @author weipeng2k 2015年3月31日 上午9:09:58
05 */
06@WebServlet(name = "NoSuchMethodErrorServlet", urlPatterns = { "/noSuchMethodError.do" })
07public class NoSuchMethodErrorServlet extends HttpServlet {
08 
09    private static final long serialVersionUID = 1699609060417354821L;
10 
11    @Override
12    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, 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 多个类加载器的环境下就会出现一些难以定位的情况。

看如下示例:

01package com.murdock.classloader.servlet;
02 
03import java.io.File;
04import java.io.IOException;
05import java.net.URL;
06 
07import javax.servlet.ServletException;
08import javax.servlet.annotation.WebServlet;
09import javax.servlet.http.HttpServlet;
10import javax.servlet.http.HttpServletRequest;
11import javax.servlet.http.HttpServletResponse;
12 
13import org.apache.mina.proxy.utils.MD4;
14 
15import com.murdock.classloader.CachedClassLoader;
16 
17/**
18 * @author weipeng2k 2015年4月4日 下午6:00:54
19 */
20@WebServlet(name = "ClassCastExceptionServlet", urlPatterns = "/classCastException.do")
21public class ClassCastExceptionServlet extends HttpServlet {
22    private static final long   serialVersionUID    = -8959000121057369987L;
23 
24    @Override
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(
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,最后将其打印到浏览器。

请求URL:http://localhost:8080/classCastException.do

响应页面,出现错误:

1java.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
响应页面,输出正常:

1org.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逻辑:

01if (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);
11else {
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 。

我们先看一下出错的异常信息,然后分析一下它产生的条件和原因:

01java.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 current class, 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在常规情况下非常难以制造,只有在多个类加载器交互时才有可能出现,下面看一下问题代码。出现问题的类和参数:

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

    测试用例如下:

01@Test
02public 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 NoClassDefFoundErrorIncompatibleClassChangeError NoSuchMethodError NoSuchFieldError IllegalAccessErrorClassCastException LinkageErrorIDE 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里面进行仲裁或者排除了。

参考原文:

http://blog.chinaunix.net/uid-21227800-id-65885.html

http://ifeve.com/classloader/


0 0
原创粉丝点击