java中自定义类加载器

1,169 阅读5分钟

类加载器

所谓类加载器, 就是用于加载java的class文件到java虚拟机中的组件, 它负责读取java字节码, 并转换成java.lang.Class类的一个实例, 使字节码.class文件得以运行. 一般类加载器负责根据一个指定的类找到对应的字节码, 然后根据这些字节码定义一个java类. 另外, 它还可以加载资源, 包括图像文件和配置文件

类加载器在实际使用中给我们带来的好处是: 它可以使java类动态地加载到jvm中并运行, 即可在程序运行时再加载类, 提供了很灵活的动态加载方式

在java中有如下已经定义好的类加载器:

  1. 启动类加载器(BootStrap ClassLoader): 加载对象是java核心库, 把一些核心的java类加载到jvm中, 这个类加载器使用原生C++代码实现, 并不是继承java.lang.ClassLoader, 它是所有其它类加载器的最终父类加载器, 负责加载%JAVA_HOME%/jre/lib目录下jvm指定的类库, 其实它是属于jvm整体的一部分, jvm一启动就将这些指定的类加载到内存中, 避免以后过多的I/O操作, 提高系统的运行效率, 该类加载器无法被java程序直接使用
  2. 扩展类加载器(Extension ClassLoader): 加载的对象为java的扩展库, 即加载%JAVA_HOME%/jre/lib/ext里面的类, 这个类由启动类加载器加载, 但因为启动类加载器并非java代码实现, 已经脱离了java体系, 所以如果尝试调用扩展类加载器的getParent()方法获取父类加载器会得到null的返回, 然而它的父类加载器确实是BootStrap ClassLoader
  3. 应用程序类加载器(Application ClassLoader): 也叫作系统类加载器(System ClassLoader), 它负责加载用户类路径(classpath)指定的类库, 如果程序没有自定义类加载器, 就默认使用应用程序类加载器, 它也由启动类加载器加载, 但是它的父类加载器是Extension ClassLoader, 如果要使用这个类加载器, 可以通过ClassLoader中提供的静态方法获取: ClassLoader.getSystemClassLoader()
public static void main(String[] args) {
    // java程序启动的时候, 可以通过-D参数配置当前的这个java进程的系统类加载器. 但是一般不会去配置该参数, 默认的就是AppClassLoader
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    // sun.misc.Launcher$AppClassLoader@18b4aac2
    System.out.println(systemClassLoader);
    // sun.misc.Launcher$ExtClassLoader@1b6d3586
    System.out.println(systemClassLoader.getParent());
    // null: 因为启动类加载器并非java代码实现, 已经脱离了java体系, 所以如果尝试调用扩展类加载器的getParent()方法获取父类加载器会得到null的返回
    System.out.println(systemClassLoader.getParent().getParent());
    /**
     * idea中运行main方法的时候, 后面就会带上classpath, 可以看到有/jre/lib下的, 由BootStrap类加载器加载
     *                                                   有/jre/lib/ext下的, 由ExtClassLoader加载
     *                                                   有自己项目/targer/classes下的, 由AppClassLoader加载
     * -classpath
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\charsets.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\deploy.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\access-bridge-64.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\cldrdata.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\dnsns.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\jaccess.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\jfxrt.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\localedata.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\nashorn.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunec.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunjce_provider.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunmscapi.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\sunpkcs11.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\ext\zipfs.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\javaws.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jce.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jfr.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jfxswt.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\jsse.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\management-agent.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\plugin.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\resources.jar;
     * F:\java\jdk1.8\jdk1.8.0_291\jre\lib\rt.jar;
     * F:\code\tomcattest\target\classes com.darkness.comcattest.classloader.ParentaDelegationTest
     */
}

如果使用ExtClassLoader加载classpath下的类会怎样?

public static void main(String[] args) throws Exception {
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    // 使用ExtClassLoader加载不了classpath下的类
    Class<?> aClass = systemClassLoader.getParent().loadClass("com.darkness.comcattest.classloader.Test");
    System.out.println(aClass.getClassLoader());
}
报错如下
Exception in thread "main" java.lang.ClassNotFoundException: com.darkness.comcattest.classloader.Test
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at com.darkness.comcattest.classloader.TestClassLoader.main(TestClassLoader.java:12)

classpath下的类必须使用AppClassLoader加载

public static void main(String[] args) throws Exception {
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    Class<?> aClass = systemClassLoader.loadClass("com.darkness.comcattest.classloader.Test");
    System.out.println(aClass.getClassLoader());
}
输出如下
sun.misc.Launcher$AppClassLoader@18b4aac2

为什么第一种会报错?就需要看看java中类加载器是如何执行loadClass方法的

双亲委派机制

java中类加载是使用双亲委派机制, 说简单点, 就是父类加载器能加载的, 就不会给子类加载器加载.

loadClass方法执行逻辑

点开loadClass方法, 发现跳出了3个选择

image.png 所以只能先看看AppClassLoader中的loadClass(String str), 发现没有, 看看AppClassLoader的类继承关系 image.png 可以看出AppClassLoader是继承自如上3个ClassLoader的, 所以唯一可能就是调用了ClassLoader的loadClass方法. 代码如下

类名: java.lang.ClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

发现调用的其实是loadClass(String name, Boolean resolve), 但是这个方法会走到哪里呢? 实际上还是走到了子类AppClassLoader中的方法中去. 这是一个模板方法设计模式, 我们看看AppClassLoader的loadClass方法是什么样吧

类名: sun.misc.Launcher.AppClassLoader
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
    // 46是., var1就是传过来的类名的全限定名, 如此截取可以得到SimpleClassName
    int var3 = var1.lastIndexOf(46);
    if (var3 != -1) {
        // 如果传过来的类全限定名没问题, 就会看看是不是开启了安全管理器
        // 这个不管, 一般是null, 不会进入下面这个if
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            var4.checkPackageAccess(var1.substring(0, var3));
        }
    }
    // ucp是UrlClassPath的实例, 里面有类的加载路径, 但是这个方法一般是会返回false的, 直接进入下面的else分支
    if (this.ucp.knownToNotExist(var1)) {
        Class var5 = this.findLoadedClass(var1);
        if (var5 != null) {
            if (var2) {
                this.resolveClass(var5);
            }

            return var5;
        } else {
            throw new ClassNotFoundException(var1);
        }
    } else {
        // 最终还是调用的父类的loadClass方法
        return super.loadClass(var1, var2);
    }
}

下面看看父类的loadClass方法是怎么实现的

类名: java.lang.ClassLoader
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 这个方法里面首先check一下name, 校验name通过后, 就会调用一个native方法, 看看在jvm中是否已经加载过这个类
        // 这个方法中check的name仅仅只是校验格式正不正确(为什么莫名其妙在这里提这一嘴, 校验名字难道除了校验名字的格式之外, 还会校验其它的东西? 后面会讲到)
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 拿到parent, 父类加载器, 使用父类加载器去递归调用父类加载器的loadClass方法
                if (parent != null) {
                    // parent不为空, 就是ExtClassLoader, ExtClassLoader的loadClass方法和AppClassLoader基本一样, 最终也还是会调到父类ClassLoader中的这个方法中来
                    c = parent.loadClass(name, false);
                } else {
                    // parent为空, 就只能是BootStrap类加载器了, 调用顶级类加载器去加载类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 如果父类加载器加载的类为null, 也就是没加载到
            if (c == null) {
                long t1 = System.nanoTime();
                // 调用自己的findClass方法去找类, 如果没找到, 由于这是一个递归的过程, 没找到之后又会归到子类加载器的代码中去findClass, 这就是双亲委派的原理
                // 这里会真正的去加载磁盘中的class文件到内存中
                c = findClass(name);
                // 加载到Class对象之后, 做一些记录操作
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            // 链接指定的类
            // 如果类 c 已经被链接,那么这个方法简单地返回。否则,将按照Java语言规范的“执行”一章中的描述链接该类
            resolveClass(c);
        }
        // 最终返回加载到的c: Class<?>类型
        return c;
    }
}

可以看到里面最终真正加载磁盘.class文件的其实是findClass方法, 下面就看看findClass是怎么做的

类加载器加载磁盘中的类: findClass方法

我们通过看类的继承关系, 知道了AppClassLoader的父类有哪些: URLClassLoader, SecureClassLoader, ClassLoader, 发现在URLClassLoader中重写了findClass(String name)

类名: java.net.URLClassLoader
protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        // 调用AccessController的一个静态本地方法doPrivileged
        // 传入了一个PrivilegedExceptionAction接口类型的匿名内部类, 实现的run方法return了一个defineClass方法
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

别的我们没必要看, 我们看到传入的匿名内部类return了一个defineClass, 而这个返回正好又是Class<?>类型, 说明很可能读取磁盘.class文件的就是这个defineClass方法, 通过查找可以发现, 在ClassLoader中, 有这么一个defineClass方法.

类名: java.lang.ClassLoader
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError {
    return defineClass(name, b, off, len, null);
}

说明defineClass并不是读取.class文件到磁盘的方法, 而是把输入流转成的字节数组格式进而构造成一个Class对象的方法. 真正从磁盘加载.class文件的代码肯定在调用defineClass之前. 所以我们可以尝试着自己继承一下ClassLoader类, 去读取一个磁盘中随便一个位置的Test.class, 实现一个自己的类加载器, 看看能不能获取成功一个Test类的Class对象. 由于要加载磁盘随便位置的类, 所以必须要打破双亲委派机制.

打破双亲委派

public class MyClassLoader extends ClassLoader {
    private String name;
    public MyClassLoader(ClassLoader parent, String name) {
        super(parent);
        this.name = name;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            FileInputStream in = new FileInputStream("F:\myclassloader\Test.class");
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] readBytes = new byte[1024];
            int len;
            while ((len = in.read(readBytes)) != -1) {
                out.write(readBytes, 0, len);
            }
            byte[] bytes = out.toByteArray();
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return findClass(name);
    }
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader(MyClassLoader.class.getClassLoader(), "myClassLoader");
        Class<?> aClass = myClassLoader.loadClass("com.darkness.comcattest.classloader.Test");
        System.out.println(aClass.getClassLoader());
    }
}
运行报错:
java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:754)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.darkness.comcattest.classloader.MyClassLoader.findClass(MyClassLoader.java:32)
	at com.darkness.comcattest.classloader.MyClassLoader.loadClass(MyClassLoader.java:42)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.darkness.comcattest.classloader.MyClassLoader.findClass(MyClassLoader.java:32)
	at com.darkness.comcattest.classloader.MyClassLoader.loadClass(MyClassLoader.java:42)
	at com.darkness.comcattest.classloader.MyClassLoader.main(MyClassLoader.java:47)
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:635)
	at com.darkness.comcattest.classloader.MyClassLoader.findClass(MyClassLoader.java:32)
	at com.darkness.comcattest.classloader.MyClassLoader.loadClass(MyClassLoader.java:42)
	at com.darkness.comcattest.classloader.MyClassLoader.main(MyClassLoader.java:47)

上面的代码为什么会报错? 通过异常堆栈可以看到, 一共调用了2次MyClassLoader.loadClass(MyClassLoader.java:42)和2次MyClassLoader.findClass(MyClassLoader.java:32) 通过debug调试可以看到, 之所以调用2次, 是因为第一次进去name确实是com.darkness.comcattest.classloader.Test, 但是加载的时候发现, 它还有一个父类Object, 所以转头就去加载name=java.lang.Object, 我们可以看到最终报错的地方是: at java.lang.ClassLoader.preDefineClass(ClassLoader.java:655), 我们可以看看这个方法

类名: java.lang.ClassLoader
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
    if (!checkName(name))
        throw new NoClassDefFoundError("IllegalName: " + name);
    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    // 校验名字, 不能以java.开头
    if ((name != null) && name.startsWith("java.")) {
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
        pd = defaultDomain;
    }
    if (name != null) checkCerts(name, pd.getCodeSource());
    return pd;
}

这正是前面提到的校验name, name是有要求的, 自定义的类加载器, 是不允许加载java.开头的类的(需要提一嘴, AppClassLoader和ExtClassLoader是可以加载java.开头的类的, 它们都是Lanucher类的受保护的静态内部类, 外部是无法访问的, 它们不属于自定义类加载器), 所以我们自定义的类加载器加载java.lang.Object的时候, 就会报错了, 抛一个安全异常SecurityException, 因为这样是不安全的 所以我们只需要改动loadClass方法, 让父类加载器去加载Object类

类名: com.darkness.comcattest.classloader.MyClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass(name);
    if (aClass != null)
        return aClass;
    return findClass(name);
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2

报错是不报了, 结果为什么我加载的Test类的类加载器是AppClassLoader呢? 那我自定义的类加载器不是白干了吗? 实际上是因为双亲委派的原因, 虽然我class类路径是F:\myclassloader\Test.class, 但是我在项目中有Test类啊, 我开头就用AppClassLoader去加载类, 加载不到我才会用自定义的findClass去读取, 但是AppClassLoader是能加载到classpath下的Test类的, 所以这样打印就会是AppClassLoader, 但是如何解决呢? 很简单, 我让ExtClassLoader去加载Object类就行了, 代码如下

类名: com.darkness.comcattest.classloader.MyClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> aClass = null;
    try {
        // 这里一定要try catch住, 因为使用ExtClassLoader是一定不可能加载到Test的, F:\myclassloader\Test.class它是肯定加载不到的, classpath下的那个Test它一样是加载不到, 这样就可以让我自定义的loadClass方法走到我自己的findClass方法去读取我F盘的Test类
        aClass = ClassLoader.getSystemClassLoader().getParent().loadClass(name);
    } catch (ClassNotFoundException e) {
    }
    if (aClass != null)
        return aClass;
    return findClass(name);
}
运行结果:
com.darkness.comcattest.classloader.MyClassLoader@1b6d3586

所谓打破双亲委派, 实际上就是绕过AppClassLoader, 让java自带的类加载器无法加载classpath下的类, catch住ClassNotFoundException后, 去执行我们自己的读取class文件的方法, 但是这样做的意义又是什么呢? 看下面一个例子:

类名: com.darkness.comcattest.classloader.MyClassLoader
public static void main(String[] args) throws Exception {
    MyClassLoader myClassLoader1 = new MyClassLoader(MyClassLoader.class.getClassLoader(), "myClassLoader");
    MyClassLoader myClassLoader2 = new MyClassLoader(MyClassLoader.class.getClassLoader(), "myClassLoader2");
    Class<?> aClass1 = myClassLoader1.loadClass("com.darkness.comcattest.classloader.Test");
    Class<?> aClass2 = myClassLoader2.loadClass("com.darkness.comcattest.classloader.Test");
    System.out.println(aClass1 == aClass2);
}
运行结果:
false

上面我new了2个我自定义的类加载器, 去加载同一个类Test, 执行的同一套方法, F盘上的Test.class文件也是同一个, 但是加载到的Class对象却不是同一个. 这就说明: ClassLoader的对象不同, 加载同一个类的Class对象也一定不是同一个. 这区别就很明显了, 如果我们使用AppClassLoader, 让它去加载classpath下的类, 由于它是java自带的系统级的类加载器, 固然是单例的, 那么每次我们获取到的AppClassLoader对象都是同一个, 如果让它去加载同一个类, 每次得到的Class对象也都会是同一个(因为它会去检查是否加载过), 而AppClassLoader还是Lanucher类的一个受保护的static class(静态内部类), 外部也是无法去建立它的其它对象的. 所以我们只能自定义类加载器, 能够无限创建自定义类加载器对象去加载类, 并且要绕过AppClassLoader. tomcat也就是这么做的. 它会给每个Context创建一个自定义的类加载器去加载类(WebAppClassLoader), 目的就是防止版本冲突.