打破双亲委派模型的场景
- 实现热插拔、热部署:在一些框架如OSGi中,需要动态加载和替换类,打破双亲委派模型可以实现不同模块使用不同版本的类,避免版本冲突。
- 自定义类加载需求:某些特殊业务场景,如需要从特定位置(如数据库、网络等非常规路径)加载类,而不是按照双亲委派模型从常规的系统类路径加载。
打破双亲委派模型的方式
- 自定义类加载器:继承
ClassLoader
类,重写loadClass
方法,在该方法中不调用父类的loadClass
方法,而是自己实现类的加载逻辑。例如:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查该类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 自己实现类加载逻辑,例如从指定路径读取class文件并转化为字节数组
byte[] classData = loadClassData(name);
c = defineClass(name, classData, 0, classData.length);
} catch (Exception e) {
// 如果自定义加载失败,再尝试使用父类加载器加载
c = getParent().loadClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private byte[] loadClassData(String className) throws Exception {
// 从自定义位置(如文件系统、网络等)读取class文件并返回字节数组
// 示例:从文件系统读取
File file = new File("custom-classes/" + className.replace('.', '/') + ".class");
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1) {
bos.write(buffer, 0, length);
}
fis.close();
bos.close();
return bos.toByteArray();
}
}
- 线程上下文类加载器:通过
Thread.currentThread().setContextClassLoader
设置线程上下文类加载器,在需要打破双亲委派的地方,使用该线程上下文类加载器加载类。例如在JDBC的DriverManager
中,就是通过线程上下文类加载器加载数据库驱动,因为数据库驱动通常是由第三方提供,不适合由启动类加载器或扩展类加载器加载。
打破后可能带来的风险
- 类冲突问题:可能会导致同一个类被不同的类加载器加载,从而在内存中存在多个不同的类实例,在进行类型比较、对象转换等操作时可能会出现异常。例如,一个类
A
被系统类加载器加载和自定义类加载器加载,这两个A
类虽然类名相同,但实际是不同的类型,在进行instanceof
判断等操作时会得到错误结果。
- 安全风险:打破双亲委派模型可能绕过系统类加载器的安全检查机制。系统类加载器通常会对加载的类进行一定的安全验证,自定义类加载器如果不进行同样严格的验证,可能会加载恶意类,从而导致安全漏洞,如恶意代码注入等问题。
- 维护复杂性增加:由于打破双亲委派模型后,类的加载逻辑变得复杂,不同模块可能使用不同的类加载器,这增加了代码维护的难度,在排查类加载相关问题时更加困难。