【基础篇】六、自定义类加载器打破双亲委派机制
文章目录
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方法
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!