Java自定义类加载器与双亲委派模型

类加载器

加载类的开放性

类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因。在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字节流,完成这个动作的代码块就是类加载器。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。

虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:

  • 从ZIP包中读取,这很常见,成为JAR,EAR,WAR格式的基础
  • 从网络中获取,最典型的应用就是Applet
  • 运行时计算生成,最典型的是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  • 有其他文件生成,最典型的JSP应用,由JSP文件生成对应的Class类
    ……

类加载器与类的唯一性

类加载器虽然只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性。通俗的说,JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。

这里的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

以下代码说明了不同的类加载器对instanceof关键字运算的结果的影响。

package com.jvm.classloading;

import java.io.IOException;
import java.io.InputStream;

/**
 * 类加载器在类相等判断中的影响
 * 
 * instanceof关键字
 * 
 */

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 自定义类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(fileName);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);   
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };

        // 使用ClassLoaderTest的类加载器加载本类
        Object obj1 = ClassLoaderTest.class.getClassLoader().loadClass("com.jvm.classloading.ClassLoaderTest").newInstance();
        System.out.println(obj1.getClass());
        System.out.println(obj1 instanceof com.jvm.classloading.ClassLoaderTest);

        // 使用自定义类加载器加载本类
        Object obj2 = myLoader.loadClass("com.jvm.classloading.ClassLoaderTest").newInstance();
        System.out.println(obj2.getClass());
        System.out.println(obj2 instanceof com.jvm.classloading.ClassLoaderTest);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

输出结果:

class com.jvm.classloading.ClassLoaderTest
true
class com.jvm.classloading.ClassLoaderTest
false
  • 1
  • 2
  • 3
  • 4

myLoader是自定义的类加载器,可以用来加载与自己在同一路径下的Class文件。main函数的第一部分使用系统加载主类ClassLoaderTest的类加载器加载ClassLoaderTest,输出显示,obj1的所属类型检查正确,这是虚拟机中有2个ClassLoaderTest类,一个是主类,另一个是main()方法中加载的类,由于这两个类使用同一个类加载器加载并且来源于同一个Class文件,因此这两个类是完全相同的。

第二部分使用自定义的类加载器加载ClassLoaderTest,class com.jvm.classloading.ClassLoderTest显示,obj2确实是类com.jvm.classloading.ClassLoaderTest实例化出来的对象,但是第二句输出false。此时虚拟机中有3个ClassLoaderTest类,由于第3个类的类加载器与前面2个类加载器不同,虽然来源于同一个Class文件,但它是一个独立的类,所属类型检查是返回结果自然是false。

双亲委派模型

类加载器种类

从Java虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。

从开发者的角度,类加载器可以细分为:

  • 启动(Bootstrap)类加载器:负责将 Java_Home/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  • 标准扩展(Extension)类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  • 应用程序(Application)类加载器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般称为系统(System)加载器

除此之外,还有自定义的类加载器,它们之间的层次关系被称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

类加载器的双亲委派模型

双亲委派模型

双亲委派模型过程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

双亲委派模型的系统实现

在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name,false);
            }else{
                c = findBootstrapClassOrNull(name);
            }
        }catch(ClassNotFoundException e){
            //if throws the exception ,the father can not complete the load
        }
        if(c == null){
            c = findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

注意,双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是JDK中也有较大规模破坏双亲模型的情况,例如线程上下文类加载器(Thread Context ClassLoader)的出现,具体分析可以参见周志明著《深入理解Java虚拟机》。



其实,双亲委派模型并不复杂。自定义类加载器也不难!随便从网上搜一下就能搜出一大把结果,然后copy一下就能用。但是,如果每次想自定义类加载器就必须搜一遍别人的文章,然后复制,这样显然不行。可是自定义类加载器又不经常用,时间久了容易忘记。相信你经常会记不太清loadClassfindClassdefineClass这些函数我到底应该重写哪一个?它们主要是做什么的?本文大致分析了各个函数的流程,目的就是让你看完之后,难以忘记!或者说,延长你对自定义类加载器的记忆时间!随时随地想自定义就自定义!

1. 双亲委派模型

关于双亲委派模型,网上的资料有很多。我这里只简单的描述一下,就当是复习。

1.1 什么是双亲委派模型?

首先,先要知道什么是类加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。如果站在JVM的角度来看,只存在两种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。

  • 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:

    • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
    • 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。


类加载器的双亲委派模型

1.2 为什么需要双亲委派模型?

为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

ClassLoader1ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

1.3 如何实现双亲委派模型?

双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实ClassLoader类默认的loadClass方法已经帮我们写好了,我们无需去写。

2. 自定义类加载器

2. 1几个重要函数

2.1.1 loadClass

loadClass默认实现如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

再看看loadClass(String name, boolean resolve)函数:

复制代码
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(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

话句话说,如果自定义类加载器,就必须重写findClass方法!

2.1.1 find Class

findClass的默认实现如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

可以看出,抽象类ClassLoaderfindClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.

如果是是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为Class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象啦~

2.1.1 defineClass

defineClass主要的功能是:

将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则需要解密后作为形参传入defineClass函数。

defineClass默认实现如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError  {
        return defineClass(name, b, off, len, null);
}

2.2 函数调用过程

上一节所提的函数调用过程如下:


自定义函数调用过程

2.3 简单示例

首先,我们定义一个待加载的普通Java类:Test.java。放在com.huachao.cl包下:

复制代码
package com.huachao.cl;

public class Test {
    public void hello() {
        System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass()
                + " 加载进来的");
    }
}
复制代码

注意:

如果你是直接在当前项目里面创建,待Test.java编译后,请把Test.class文件拷贝走,再将Test.java删除。因为如果Test.class存放在当前项目中,根据双亲委派模型可知,会通过sun.misc.Launcher$AppClassLoader 类加载器加载。为了让我们自定义的类加载器加载,我们把Test.class文件放入到其他目录。

在本例中,我们Test.class文件存放的目录如下:


class文件目录

接下来就是自定义我们的类加载器:

复制代码
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class Main {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;

        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    };

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.huachao.cl.Test");
        Object obj = clazz.newInstance();
        Method helloMethod = clazz.getDeclaredMethod("hello", null);
        helloMethod.invoke(obj, null);
    }
}
复制代码

最后运行结果如下:

恩,是的,我是由 class Main$MyClassLoader 加载进来的

猜你喜欢

转载自blog.csdn.net/qq_29663071/article/details/81003908