一句话总结
打破双亲委派有三种方式:① 重写 loadClass()(改变加载顺序,如 Tomcat 的 WebappClassLoader 优先自己加载)→ ② 线程上下文类加载器(Thread.setContextClassLoader(),SPI 机制的核心,让 Bootstrap 加载的类能调用用户类)→ ③ SPI + ServiceLoader(JDK 内置的打破机制,核心 API 定义接口,第三方实现,通过线程上下文类加载器加载实现类)。典型场景:JDBC 驱动加载、Tomcat 应用隔离、OSGi 模块化、热部署。
初级理解
三种打破方式
| 方式 | 原理 | 典型场景 |
| 重写 loadClass() | 改变"先委托父加载器"的顺序 | Tomcat WebappClassLoader |
| 线程上下文类加载器 | 通过 Thread 传递 AppClassLoader | SPI(JDBC、JNDI) |
| SPI + ServiceLoader | 核心库定义接口,第三方实现 | JDBC 驱动、SLF4J |
方式一:重写 loadClass()
// 默认 loadClass():先委托父加载器
protected Class<?> loadClass(String name, boolean resolve) {
// ① 检查是否已加载
// ② 委托父加载器
// ③ 父加载器找不到 → 自己 findClass()
}
// 打破方式:重写 loadClass(),先自己加载
protected Class<?> loadClass(String name, boolean resolve) {
// ① 检查是否已加载
// ② 先自己 findClass()(打破双亲委派!)
// ③ 自己找不到 → 委托父加载器
}
// 注意:JDK 核心类(java.*)仍必须由 Bootstrap 加载
// 否则会有安全问题
中级深入
方式二:线程上下文类加载器(SPI 核心)
// 问题:JDBC 的 DriverManager 在 rt.jar 中,由 Bootstrap 加载
// 但 MySQL 驱动在 classpath 中,由 AppClassLoader 加载
// Bootstrap 加载的类无法访问 AppClassLoader 加载的类!
// 解决:线程上下文类加载器
// DriverManager 通过线程上下文获取 AppClassLoader 来加载驱动
// DriverManager 源码(简化)
public class DriverManager {
static {
loadInitialDrivers(); // 使用 SPI 加载驱动
}
private static void loadInitialDrivers() {
// 获取线程上下文类加载器(默认是 AppClassLoader)
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
for (Driver d : loader) {
registerDriver(d); // 注册驱动
}
}
}
// 线程上下文类加载器设置
Thread.currentThread().setContextClassLoader(myClassLoader);
// 默认值:主线程继承 System ClassLoader(AppClassLoader)
SPI 机制完整流程
// SPI(Service Provider Interface):
// 1. 核心库定义接口(如 java.sql.Driver)
// 2. 第三方提供实现(如 com.mysql.cj.jdbc.Driver)
// 3. META-INF/services/ 下配置文件指定实现类
// 4. ServiceLoader 通过线程上下文类加载器加载实现
// META-INF/services/java.sql.Driver 文件内容:
// com.mysql.cj.jdbc.Driver
// ServiceLoader 加载流程:
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
// 内部:
// 1. 获取线程上下文类加载器
// 2. 读取 META-INF/services/java.sql.Driver
// 3. 加载并实例化文件中列出的所有实现类
高级拓展
Tomcat 类加载架构
// Tomcat 的类加载器层次(违反双亲委派)
Bootstrap ClassLoader
└── System ClassLoader
└── Common ClassLoader(Tomcat 和 Web 应用共享)
├── Catalina ClassLoader(Tomcat 自身,隔离)
├── Shared ClassLoader(所有 Web 应用共享)
└── Webapp ClassLoader(每个应用独立,打破双亲委派)
// WebappClassLoader 的加载顺序(打破双亲委派):
// 1. 先从自己缓存中查找
// 2. 如果没找到,先从 Bootstrap 加载(安全限制)
// 3. 如果没找到,先从自己路径加载(打破!优先自己)
// 4. 如果没找到,再委托父加载器
// 目的:
// 1. 不同 Web 应用的类隔离(App1 的 User 类 ≠ App2 的 User 类)
// 2. Web 应用可以覆盖父加载器的类(如覆盖 Tomcat 的 jar)
// 3. 热部署:销毁旧的 WebappClassLoader,创建新的
自定义类加载器实现热部署
// 热部署原理:用新的类加载器重新加载类
public class HotDeployClassLoader extends ClassLoader {
private String classPath;
public HotDeployClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadClassData(name);
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassData(String name) {
// 从 classPath 读取 .class 文件
String path = classPath + name.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path)) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) != -1) bos.write(buf, 0, len);
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 使用:每次修改类后,创建新的类加载器重新加载
// 旧的类加载器和旧类会被 GC 回收
实战场景
场景:同一个类不同版本共存
// 问题:应用依赖 A.jar(v1) 和 B.jar,B.jar 依赖 A.jar(v2)
// 两个版本的 A.jar 如何共存?
// 解决:用不同的类加载器加载
ClassLoader cl1 = new MyClassLoader("lib/v1/");
ClassLoader cl2 = new MyClassLoader("lib/v2/");
Class<?> classA_v1 = cl1.loadClass("com.example.A");
Class<?> classA_v2 = cl2.loadClass("com.example.A");
// classA_v1 != classA_v2(不同类加载器加载的同一个类 ≠ 同一个类)
// 这就是 OSGi 和 Tomcat 实现类隔离的原理
面试模拟
Q:如何打破双亲委派模型?
A:三种方式:1) 重写 loadClass():改变加载顺序,先自己加载再委托父加载器(Tomcat);2) 线程上下文类加载器:通过 Thread.setContextClassLoader() 传递 AppClassLoader,让 Bootstrap 加载的类能调用用户类(SPI 核心);3) SPI + ServiceLoader:核心库定义接口,通过线程上下文类加载器加载第三方实现(JDBC)。
Q:SPI 机制是什么?为什么需要打破双亲委派?
A:SPI(Service Provider Interface)是 JDK 内置的服务发现机制。核心库定义接口(如 java.sql.Driver),第三方在 META-INF/services/ 下配置实现类,ServiceLoader 加载。需要打破双亲委派是因为:核心库由 Bootstrap 加载,无法访问 AppClassLoader 加载的第三方实现类,所以通过线程上下文类加载器来加载实现类。
Q:Tomcat 为什么要打破双亲委派?
A:1) 应用隔离:不同 Web 应用的类互不影响(各自 WebappClassLoader);2) 版本覆盖:Web 应用可以用自己的 jar 覆盖 Tomcat 的 jar;3) 热部署:销毁旧 WebappClassLoader 创建新的,实现不重启更新应用;4) 安全:防止应用之间的类互相访问。