文章目录
- 1、ClassLoader抽象类的方法源码
- 2、打破双亲委派机制:自定义类加载器重写loadclass方法
- 3、自定义类加载器默认的父类加载器
- 4、两个自定义类加载器加载相同限定名的类,不会冲突吗?
- 5、一点思考
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 statsPerfCounter.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;}}@Overridepublic 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开头时,则交给父类去加载:
@Overridepublic 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方法