自定义类加载器
24 Jun 2021 | Java |假设,现在有一个Hello.xlass
文件,里面有一个hello()
方法,但是此文件内容是一个所有字节(x=255-x)被处理后的文件,那么你应该如何正确读取这个文件呢?
这里就需要自定义类加载器,来加载这个文件了。
首先,我们还是看一下Java
的类加载过程。
类的生命周期 1.加载:找Class文件 2.验证:验证格式和依赖 3.准备:为类变量(static修饰的变量)分配内存并设置初始零值。注意,此时实例变量还没有分配内存。 4.解析:符号解析为引用 5.初始化:构造器,实例变量分配内存并赋值,静态变量赋值,静态代码块 6.使用 7.卸载
三类加载器 启动类加载器 扩展类加载器 应用类加载器
加载器的特点:双亲委派;负责依赖;缓存加载。
对于双亲委派模型:一个类加载器在进行加载时,不会自己去尝试加载这个类。先看看有没有加载过这个类,然后将这个请求委派给父类加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会自己去尝试加载这个类。
这样做的好处是,防止同名的类出现混乱,也能提高安全性。举个例子,比如
java.lang.Object
这个类,无论哪个类加载器加载时,最终都会委派给Bootstrap加载器去加载,这就保证了整个系统运行过程中的Object
都是同一个类。否则,如果用户自己编写了一个java.lang.Object
类,并放在程序的classpath
中,最终系统将会出现多个不同的Object类,整个Java体系就变得一团混乱了。
在实现自己的ClassLoader
之前,先看一下JDK
中的ClassLoader
是怎么实现的:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1. 首先,检查是否被加载过了
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 没有加载过,就交给父类去加载
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();
// 3. 还是没有加载到,才交给自己去加载
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、首先查找.class
是否被加载过。
2、如果.clas
s文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null
,那么就执行父加载器的loadClass
方法,把类加载请求一直向上抛,直到父加载器为null
(这个时候就到了Bootstrap ClassLoader
)为止。
3、如果父加载器没有成功,就交给子类加载器去加载。
看一下findClass
这个方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
没有任何实现,直接抛出了一个异常,而且是protected的,所以:这个方法就是用于继承后,需要重写。
从上面的分析可以知道:
1、如果不想打破双亲委派模型,那么只需要重写findClass
方法即可。
2、如果想打破双亲委派模型,那么就重写整个loadClass
方法。
自定义的类加载器如下:
- 重写
findClass
方法; - 获取字节数组;
- 根据要求处理字节数组;
- 使用新的字节数组定义类。
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* 自定义类加载器
* <p>
* 参考资料:
* https://www.cnblogs.com/xrq730/p/4847337.html
* https://segmentfault.com/a/1190000012925715
* https://github.com/sodawy/JAVA-000/tree/main/Week_01
*/
public class MyClassLoader extends ClassLoader {
public static final byte DIGITAL_255 = (byte) 255;
private String filePath;
public MyClassLoader(String filePath) {
this.filePath = filePath;
}
//重写该方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = getClassBytes(filePath);
// 根据要求处理字节码
byte[] deBytes = handleByte(bytes);
// 使用新的字节数组定义类
return defineClass(name, deBytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
/**
* 根据自定义的要求处理字节码
*
* @param oldBytes 原来的字节数组
* @return 放回处理后的数组
*/
private byte[] handleByte(byte[] oldBytes) {
byte[] newBytes = new byte[oldBytes.length];
for (int i = 0; i < oldBytes.length; i++) {
newBytes[i] = (byte) (DIGITAL_255 - oldBytes[i]);
}
return newBytes;
}
/**
* 获取字节数组
* @param filePath class文件路径
* @return 字节数组
* @throws Exception
*/
private byte[] getClassBytes(String filePath) throws Exception {
return Files.readAllBytes(Paths.get(filePath));
}
}
自定义类加载器写完后,进行测试:
import org.junit.Test;
import java.lang.reflect.Method;
public class TestMyClassLoader {
@Test
public void test() throws Exception {
String filePath = "D:\\Hello\\Hello.xlass";
//使用自定义类加载器加载类
MyClassLoader myClassLoader = new MyClassLoader(filePath);
Class<?> clazz = myClassLoader.loadClass("Hello");
//实例化对象
Object obj = clazz.newInstance();
//获取声明的方法
Method method = clazz.getDeclaredMethod("hello");
//方法调用
method.invoke(obj);
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());
}
}
至此,我们就完成了如何自定义类加载器。
自定义类加载器的应用
Tomcat
上可以部署多个不同的应用,但是它们可以使用同一份类库的不同版本。- 对于非
.class
的文件,需要转为Java类,就需要自定义类加载器。 - 加密解密。