回答
问到双亲委派模型了,一般都会进一步问如何打破双亲委派模型!
双亲委派模型就是当一个类加载器尝试加载某个类时,它首先会把这个任务委托给他的父类加载器。只有在父类加载器无法完成加载任务时,子类加载器才会尝试自己去加载这个类。这种机制可以确保 Java 核心库的类型安全,并避免类的重复加载。
所以要打破这个双亲委派,我们就打破这个类加载的过程就行了。目前有两种比较好的方案:
- **自定义类加载器:**我们自定义一个类加载器,重写
loadClass()
方法,并不做双亲委派实现即可,比如我们先自己尝试加载类,而不是先委托给父类加载器。 - 使用线程上下文类加载器:线程上下文类加载器(Thread Context ClassLoader,简称
TCCL
)是一种间接打破双亲委派模型的的方法。通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作来打破双清委派模型。
扩展
为什么要打破双亲委派模型
虽然双亲委派模型提供了一些好处,如避免类重复加载、确保 Java 核心库的类型安全,但是在一些特定的场景,打破这个模型是很有必要的。
- 特定环境下的类加载需求:在一些复杂的应用环境中,比如容器(如Tomcat)、OSGi环境,他们需要加载同一个类的不同版本或者加载一些由容器提供而不是应用程序带有的类。在这种情况下,标准的双亲委派模型是无法满足需求的,所以需要通过自定义类加载器来打破双亲委派模型,实现更灵活的加载策略。
- 热部署:热部署,在开发大型应用时,是一个比较常见的操作。为了实现热部署,我们希望能够重新加载已经改变的最新版本的类,这就需要绕过标准的双亲委派模型。
如何打破双亲委派模型详解
自定义类加载器
使用自定义类加载器破坏双亲委派模式基本步骤如下:
- 继承 ClassLoader 类:新建一个类加载器,继承
java.lang.ClassLoader
。 - 重写
loadClass()
方法:重写该方法时,我们先尝试自己加载类,如果加载不到,再去按照正常的双亲委派模型去加载。
示例如下:
- 自定义类加载器。继承
java.lang.ClassLoader
,重写loadClass()
方法:
public class MyClassLoader extends ClassLoader{
private String basePath;
public MyClassLoader(String basePath) {
this.basePath = basePath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 1. 先尝试自己加载
byte[] classData = new byte[0];
try {
classData = getClassData(name);
if (classData != null) {
// 定义类
return defineClass(name, classData, 0, classData.length);
}
} catch (IOException e) {
// 处理异常
}
// 如果无法加载类,委托给父加载器
return super.loadClass(name);
}
/**
* 将 .class 文件转换为二进制
* @param name
* @return
* @throws Exception
*/
private byte[] getClassData(String name) throws IOException {
name = name.replaceAll("\\.", "/");
String path = basePath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";;
File file = new File(path);
if (file.exists()) {
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int bytesNumRead = 0;
while ((bytesNumRead = fis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
}
}
return null;
}
}
- 我们再定义一个测试类
public class MyTest {
public void test() {
System.out.println("Hello,这是大明哥的 Java 面试宝典...");
}
}
- 测试下
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("根路径....");
Class clazz = classLoader.loadClass("com.skjava.java.feature.MyTest");
Object myTest1 = clazz.newInstance();
Method method = clazz.getMethod("test", null);
method.invoke(myTest1,null);
}
执行结果:
使用线程上下文类加载器
线程上下文类加载器允许我们在执行线程过程中设置一个类加载器,用来加载类。通过设置线程上下文类加载器,可以使得一些标准API 在加载类时,不再遵循标准的双亲委派模型,而是直接使用线程上下文类加载器加载所需的类。
一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能去加载ClassPath下的类。 但是有了线程上下文类加载器就好办了,JNDI服务使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。 Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。 —摘自**《深入理解java虚拟机》周志明**
实现方式如下:
在线程中,通过 Thread.currentThread().setContextClassLoader()
设置自定义的类加载器。此后,该线程中运行的代码就可以通过 Thread.currentThread().getContextClassLoader()
获取到这个类加载器,并用它来加载类。
打破双亲委派模型案例
在面试中如果问到打破双亲委派模型,一般都会进一步问到相关案例,在 Java 中两个最出名的案例就是 JDBC 和 Tomcat。
- JDBC
- Tomcat
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。