自定义类加载器验证类加载机制

  • 2021 年 8 月 23 日
  • 笔记

自定义类加载器验证类加载机制

全盘委托机制

当一个ClassLoader装载一个类时,除非显示地使用另一个ClassLoader,则该类所依赖及引用的类也由这个CladdLoader载入。

双亲委派机制

子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

几个重要的函数

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;
    }
}

1. loadClass()

可以看出loadClass()方法中实现了双亲委派的机制,即找父加载器,如果找到,则调用父加载器的loadClass(),如果父加载器为NUll,则调用启动类加载器,如果启动类加载器与父加载器都无法加载类,则调用自己的findClass()方法。

2. findClass()

很明显,如果要不改变双亲委派机制的话,只需要重写findClass()方法,实现类加载的逻辑。

自定义类加载器

各级的类加载器关系如下

自定义加载器MyLoaderA

public class MyLoaderA extends ClassLoader{
    public MyLoaderA(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            if(!name.endsWith("A")){
                // A加载器只能加载A类
                throw new ClassNotFoundException();
            }
            System.out.println("A加载器正在进行加载");
            // 读取类路径下的class文件
            String className = name.substring(name.lastIndexOf(".") + 1)+".class";
            InputStream inputStream = getClass().getResourceAsStream(className);
            if (inputStream == null){
                return super.loadClass(name, false);
            }
            byte[] bytes = new byte[inputStream.available()];

            inputStream.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException();
        }
    }
}

MyLoaderB和MyLoaderC与MyLoaderA类似,只是在if判断处将A改成B和C

自定义需要被加载的类

public class A {
    private B b = new B();
}


public class B {
    private C c = new C();
}


public class C {
}

类A的内部引用了类B,类B的内部引用了类C。

测试类

public class Test {
    public static void main(String[] args) throws Exception {
        MyLoaderC myLoaderC = new MyLoaderC(null);
        MyLoaderB myLoaderB = new MyLoaderB(myLoaderC);
        MyLoaderA myLoaderA = new MyLoaderA(myLoaderB);

        Object o = myLoaderA.loadClass("com.available.A").newInstance();
    }
}

MyLoaderC的父加载器是null,也就是启动类加载器,MyLoaderB的父加载器是MyLoaderC,MyLoaderA的父加载器是MyLoaderB。

猜想

首先进入myLoaderA的loadClass()方法,去寻找myLoaderA的父加载器,接着进入myLoaderB的loadClass(),寻找myLoaderB的父加载器,myLoaderC无法加载类A,抛出异常给myLoaderB,myLoaderB也抛出异常给myLoaderA,随后进入myLoaderA的findClass()方法中。

其次要加载类B,还是上面一套方法,只不过在myLoaderC抛出异常后,myLoaderB的findClass()加载了类B。

最后要加载类C,按照全盘委托机制,类B引用了类C,那么类C将不会进入myLoaderA的loadClass()方法,而是进入myLoaderB的loadClass()方法,开启双亲委派机制加载类。

debug验证

  1. 寻找到了myLoaderA的父加载器

  1. 寻找到MyLoaderB的父加载器

  1. MyLoaderC无法加载抛异常给MyLoaderB

  1. MyLoaderB无法加载抛异常给MyLoaderA(这步省略,没什么看头)

  2. myLoaderA加载类A

  1. 加载类B的步骤省略,关键看全盘委托机制,是否会进入myLoaderA的loadClass()方法中。

很明显在加载类C时,并没有进入myLoaderA的loadClass()中

初步结论

一个类在加载的时候,会从引用他的类的加载器自上开始执行双亲委派机制,也就是说,含有多层引用关系的类,被引用类的类加载器只能大于或等于引用类的加载器。

验证结论

将类A,类B,类C的引用关系改变如下:

public class A {
    private C c = new C();
}

public class B {
}

public class C {
    private B b = new B();
}

类A引用类C,类C引用类B

猜想

当加载完类C之后,此时引用类为类C,被引用类为类B,按照上面得出的结论,被引用类的类加载器只能大于或等于引用类的加载器,此时大于等于MyLoaderC的加载器不能加载类B,会报错。

验证

猜想成功。

一种打破双亲委派机制的场景

原生的JDBC的使用,获取数据库连接使用的是 Connection conn = DriverManager.getConnection(xx,xx,xx),DriverManager时jdk提供的,自然使用最上层的启动类加载器加载,而提供具体实现的是各大厂商如Mysql,按照上面的结论,启动类加载器必然不能加载Mysql的jar包,如果不打破双亲委派,则会报错。

线程上下文加载器

通过获取到规定好的线程上下文加载器,就可以在任何地方使用这个加载器来加载类从而打破双亲委派机制。

// 获得线程上下文加载器
Thread.currentThread().getContextClassLoader();

// 设置线程上下文加载器,如果没有设置,则为系统类加载器。
Thread.currentThread().setContextClassLoader(ClassLoader cl);

最终结论

一个类在加载的时候,会从引用他的类的加载器自上开始执行双亲委派机制,也就是说,含有多层引用关系的类,被引用类的类加载器如果不指定特定的类加载器就只能大于或等于引用类的加载器。