【基础篇】六、自定义类加载器打破双亲委派机制

发布时间:2023年12月27日

1、ClassLoader抽象类的方法源码

ClassLoader类的核心方法:

这里是引用

从一句常写的代码开始看ClassLoader这个抽象类的源码:

ClassLoader classLoader = TestJvm.class.getClassLoader();
Class<?> clazz = classLoader.loadClass("com.plat.A");

loadClass方法源码:

public Class<?> loadClass(String name) throws ClassNotFoundException {
   //传入了false,往下跟
    return loadClass(name, false);
}

往下跟:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
     //加synchronized,防止多线程下重复加载
     synchronized (getClassLoadingLock(name)) {
         // 先检查类是否已被加载,findLoadedClass往下跟是调用native方法
         Class<?> c = findLoadedClass(name);
         if (c == null) {
             long t0 = System.nanoTime();
             try {
                 //类加载器的parent属性不为空,即有父加载器
                 if (parent != null) {
                     //自己调自己,这里体现的是向上查找
                     c = parent.loadClass(name, false);
                 } else {
                     //去启动类加载器里找,往下跟是native方法
                     c = findBootstrapClassOrNull(name);
                 }
             } catch (ClassNotFoundException e) {
                 // ClassNotFoundException thrown if class not found
                 // from the non-null parent class loader
             }
			 //三个加载器用完了,c还是为空
             if (c == null) {
                 // If still not found, then invoke findClass in order
                 // to find the class.
                 long t1 = System.nanoTime();
  				 //那就调用findClass方法,它是LoadClass抽象类的空方法,给子类去实现,这是自定义类加载器的切入点和扩展点
                 c = findClass(name);

                 // this is the defining class loader; record the stats
                 PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                 PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                 PerfCounter.getFindClasses().increment();
             }
         }
         //resolve为false,则不执行resolveClass方法,即不要类生命周期里的连接阶段
         if (resolve) {
             resolveClass(c);
         }
         return c;
     }
}

源码摘要:

在这里插入图片描述

关于以上源码,做个简单的验证,上面提到loadClass源码传了false,导致没有进行类生命周期的连接阶段:

public class A02{

	static {
		System.out.println("类A02正在进行初始化阶段");
	}
}
public class LoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classLoader = LoaderTest.class.getClassLoader();
        Class<?> clazz = classLoader.loadClass("com.plat.pay.A02");
    }
}

在这里插入图片描述

发现A02类的static代码块没被执行,这就是因为这里的loadClass方法,其源码传入了false,导致resolveClass方法不执行,即后面的连接、初始化阶段都没了,而static代码块在初始化阶段执行,这和Class.forName是有本质区别的,后者连接和初始化阶段都执行。

2、打破双亲委派机制:自定义类加载器重写loadclass方法

创建一个类,继承ClassLoader抽象类,重写loadClass方法:

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;

/**
 * 打破双亲委派机制 - 自定义类加载器
 */

public class BreakClassLoader1 extends ClassLoader {

    private String basePath;
    private final static String FILE_EXT = ".class";

    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }

    private byte[] loadClassData(String name)  {
        try {
            String tempName = name.replaceAll(".", Matcher.quoteReplacement(File.separator));
            FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
            try {
                return IOUtils.toByteArray(fis);
            } finally {
                IOUtils.closeQuietly(fis);
            }

        } catch (Exception e) {
            System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
            return null;
        }
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }

   
}

写个测试类:

在这里插入图片描述

跟进报错的第二行preDefineClass方法,发现自定义加载器的父类ClassLoader中做了校验,以java开头抛安全异常,也是安全的体现:

在这里插入图片描述

换一个普通命名的包:

在这里插入图片描述

报错找不到Object,加载A类前,会先加载其父类Object,此时可拷贝个Object的class到我这个目录,也可以修改自定义加载器的实现,java开头时,则交给父类去加载:

@Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    	//如果全类名是java开头的类,就让父类加载器去办
        if(name.startsWith("java.")){
            return super.loadClass(name);
        }
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }

再测试:

public class LoaderTest {
    public static void main(String[] args) throws Exception {
        BreakClassLoader1 classLoader = new BreakClassLoader1();
        classLoader.setBasePath("D:\\springboot\\pay\\target\\classes\\");
        Class<?> clazz = classLoader.loadClass("com.plat.pay.A02");
        System.out.println(clazz.getClassLoader());
    }
}

加载成功:

在这里插入图片描述

查看自定义加载器的父加载器:

BreakClassLoader1 classLoader = new BreakClassLoader1();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
//System.out.println(BreakClassLoader1.getSystemClassLoader());

发现其父加载器是应用程序加载器:

在这里插入图片描述

在这里插入图片描述

3、自定义类加载器默认的父类加载器

复习super关键字:当构造方法的第一行,既没有this(……)又没有super(……)的时候,默认会有一个super(),表示通过当前子类的构造方法调用其父类的无参构造方法。自定义类加载器父类ClassLoader类的无参构造:

在这里插入图片描述

this是在调用本类的另一个构造方法:

在这里插入图片描述

传入的getSystemClassLoader值为一个AppClassLoader,因此,自定义类加载器默认的父类加载器。

4、两个自定义类加载器加载相同限定名的类,不会冲突吗?

不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。

public class LoaderTest {
    public static void main(String[] args) throws Exception {
        BreakClassLoader1 classLoader1 = new BreakClassLoader1();
        classLoader1.setBasePath("D:\\springboot\\pay\\target\\classes\\");
        Class<?> clazz1 = classLoader1.loadClass("com.plat.pay.A02");

        BreakClassLoader1 classLoader2 = new BreakClassLoader1();
        classLoader2.setBasePath("D:\\springboot\\pay\\target\\classes\\");
        Class<?> clazz2 = classLoader2.loadClass("com.plat.pay.A02");
        //关于==:
        //如果是基本数据类型的比较,则比较的是值。
        //如果是包装类或者引用类的比较,则比较的是对象地址
        //关于equals:
        //equals方法没有重写还是比较对象地址
        //equals方法重写后比较啥,是看重写的逻辑是啥
        System.out.println(clazz1 == clazz2);
    }
}

结果为false,即同一个类,被两个自定义加载器加载,是两个不同的Class对象
在这里插入图片描述

采用Arthas验证,在上面程序后面加一句输入,卡着让程序别退出运行:

System.in.read();

出现两次,即一个类如果由两个自定义类加载器分别去加载,在程序中会出现两个不同的class对象:

在这里插入图片描述
小补充:

//设置线程上下文的类加载器
Thread.currentThread().setContextClassLoader(new BreakClassLoader1());
//com.plat.broken.BreakClassLoader1@6537cf78
System.out.println(Thread.currentThread().getContextClassLoader());

5、一点思考

上面提到的,一个类被两个自定义类加载器去加载,会有两个class对象,那问题来了,双亲委派机制呢?不查?这是因为上面我写的自定义类加载器,直接重写了loadClass方法,而重写的实现里,没有原来的查父类(参考上面loadClass本来的源码),而是直接去指定路径把class读成一个二进制流传入。因此,如果 想在不打破双亲委派机制的前提下自定义类加载器,那正确姿势应该是重写loadClass内部调用的findClass方法,且常规开发自定义类加载器,重写的也是findClass方法,而非loadClass方法

在这里插入图片描述

比如需要在数据库中去加载字节码文件,就重写findClass方法,将数据库中的数据获取到内存中,变成一个二进制的字节数组,然后传入到defineClass方法

文章来源:https://blog.csdn.net/llg___/article/details/135253026
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。