Java中如何去自定义一个类加载器
之前写过一篇,关于 类加载器和双亲委派的文章,里边提到过可以根据自己的需要,去写一个自定义的类加载器,正好有人问这个问题,今天有时间就来手写一个自定义的类加载器,并使用这个自定义的类加载器来加载一个class字节码文件。
一、明白为什么需要用到自定义类加载器
官方给的回答是:为 Java 应用程序提供更加灵活和可定制的类加载机制,并实现类的隔离。
为了好理解,我列举一些场景:
1、拓展加载源
其实JVM除了能加载咱们本地编译好的class文件外,还可以加载其他来源的class文件,比如可以
从网络、数据库、从你指定磁盘位置 等地方加载类。
2、实现类隔离
具体来说就是,自定义类加载器可以实现类隔离,避免类之间的冲突和干扰,这个在 tomcat 里就
有大量的应用。
注意:比较两个类是否相等,只有两个类是由同一个类加载器加载的前提下才有意义,否则即使两
个类来自同一个class文件,但是由于加载它们的类加载器不同,那这两个类就不相等。也就是即
使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类,用 instanceof 这种
对比都是不同的。
二、大概了解一下ClassLoader类的源码
从上图,可以看到,ClassLoader 它是一个 抽象类,里边的方法也很多,有已经实现的方法,也有空的方法体等着继承它的子类,根据自身场景去具体的实现。
ClassLoader类里虽然方法很多,但是其中咱们最最关注的有 三个方法:
1、loadClass() 方法
loadClass() 方法是用于加载指定名称的类,双亲委派模型核心实现,这个直接由ClassLoader自己实现,一般不建议重写 这个方法。遵循双亲委派模型,首先委派给父类加载器进行加载,如果父类加载器无法加载该类,则自身进行加载
2、findClass() 方法
findClass() 方法是用于查找类的方法,它在ClassLoader里并没有具体的实现,需要由子类加载器去实现,用于查找自身命名空间中的类,自定义类加载器推荐重写这个方法
从上图可以看到 loadClass()方法里的大致逻辑,其中 findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中加载失败后,则调用自己的findClass()方法来完成类加载。但是ClassLoader里对findClass()方法没有实现,需要自己实现具体逻辑,findClass()方法通常是和下边的defineClass()方法一起使用的
可以点 findClass()方法,进去看看,可以看到,ClassLoader里findClass()方法 确实是个空实现,啥也没做,就抛出了个异常。这是个经常使用的到 非常典型的模板设计模式
3、defindClass() 方法
defindClass() 方法是用于定义类的方法,它将字节数组转换为 Class 对象,并将其添加到类加载器的命名空间中。这个方法在ClassLoader里已经实现,findClass()方法通常是和defineClass()方法一起使用的
三、自定义一个ClassLoader类加载器
了解了上边的知识后,下面就开始手写一个类加载器,并尝试着用它来加载咱们准备的一个class字节码文件。
1、自定义一个类,继承ClassLoader类,重写里边的findClass()方法
package com.cj.cl;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String path;
public MyClassLoader(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name){
String fileName = path + name + ".class";
System.out.println(fileName);
try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
) {
//用IO流去本地磁盘上读取一个class字节码文件
int len;
byte[] data = new byte[1024];
while ((len=bis.read(data))!=-1){
bos.write(data,0,len);
}
//把它转换成字节数组
byte[] bytes = bos.toByteArray();
//再把字节数组 传给defindClass() 方法,由defineClass完成类的加载
Class<?> defineClass = defineClass(null, bytes, 0, bytes.length);
return defineClass;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
自己写的这个类加载器的代码也很简单,就是用IO流去本地磁盘上读取一个class字节码文件,然后把它转换成字节数组,传给defindClass() 方法,defindClass() 方法就能将字节数组转换为 Class 对象,并将其添加到类加载器的命名空间中,完成类的加载。
2、准备一个编译好的class字节码文件
我在idea里随便写一个Student类
然后 在这个类上 点击右键
就可以直接进入该类所在的目录里了,进来之后,在下边的命令行 输入 javac Student.java
编译成功后,当前目录会多出一个编译好的class字节码文件
然后把这个class文件 剪切到 E盘
3、测试自己写的自定义类加载器
写个main方法来测试
package com.cj;
import com.cj.cl.MyClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("E:/");
//使用自己自定义的类加载器,去加载Student.class 字节码文件
Class<?> clazz = myClassLoader.loadClass("Student");
//得到class字节码后,使用反射来创建对象
Object obj = clazz.getDeclaredConstructor().newInstance();
//打印出obj这个对象所属的类名
System.out.println(obj.getClass().getName());
//打印出obj这个对象所属的类 用的类加载器的名称
System.out.println(obj.getClass().getClassLoader().getClass().getName()+"类加载器");
}
}
可以看到,自己写的类加载器,已经把我电脑E盘下的 Student.class字节码文件,加载进JVM了,并且使用自定义类加载器加载进来的字节码反射生成了一个obj对象。
下面,可以再验证一下上边说的那个:即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类,用 instanceof 这种对比都是不同的。
从下图可以看到,当前项目里的Student的字节码是由jdk.internal.loader.ClassLoaders$AppClassLoader 这个JVM内部自带的应用程序类加载器加载进来的,并且用instanceof 对比了一下两个Student,结果打印出了 false
ok,今天就写到这里吧。希望对粉丝们理解java的自定义加载器有所帮助。
纯手敲 原创不易,如果觉得对你有帮助,可以关注一下,打赏一下,感谢。