什么是双亲委派?怎么破坏?
结论先行:双亲委派模型(parent delegation model)是指类加载器收到加载请求时,先委派给父加载器,父加载器无法完成才自己加载。它的价值是保证核心类库唯一——java.lang.String 永远由启动类加载器提供,用户无法塞一个同名类进来。破坏方式有三条:重写 loadClass() 走自己的顺序(Tomcat)、用线程上下文类加载器(TCCL)让父加载器反向调用子加载器(JDBC)、以及用网状委派替代树状(OSGi)。
一、委派链路
┌──────────────────────┐
请求 ───→ │ Bootstrap ClassLoader│ ← jre/lib、JDK 9+ 的 jrt:/modules
└──────────┬───────────┘
│ 找不到
┌──────────▼───────────┐
│ Extension / Platform │ ← jre/lib/ext、JDK 9+ 的 jdk.* 模块
└──────────┬───────────┘
│ 找不到
┌──────────▼───────────┐
│ Application (System) │ ← -classpath、-cp
└──────────┬───────────┘
│ 找不到
┌──────────▼───────────┐
│ 自定义 ClassLoader │ ← 插件目录、热部署、加密字节流
└──────────────────────┘
每一层只认自己的路径,找不到就返回 null,由发起请求的那一层「自下而上」回调 findClass()。
二、loadClass 的骨架
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name); // 1. 已加载则直接返回
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false); // 2. 委派父加载器
} else {
c = findBootstrapClassOrNull(name); // 3. 父为空则找启动类加载器
}
} catch (ClassNotFoundException ignored) {
}
if (c == null) {
c = findClass(name); // 4. 父加载器都失败,自己找
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
模板方法把「找字节流」的活留给了 findClass()。所以自定义加载器应该重写 findClass() 而不是 loadClass(),这样委派语义仍然保留。
三、三条破坏路径
| 方式 | 谁在用 | 做法 | 后果 |
|---|---|---|---|
重写 loadClass | Tomcat、Spring Boot 的可执行 JAR | 先在自己目录找,找不到再委派父加载器 | 实现应用间类隔离、不同版本并存 |
| 线程上下文类加载器 | JDBC、JNDI、JAXP | Thread.currentThread().setContextClassLoader(...),父层代码通过 TCCL 反向加载 | 解决 SPI 场景下父加载器看不见实现类的问题 |
| 网状委派 | OSGi、部分插件框架 | 按 Bundle 依赖关系横向委派,而非固定父链 | 支持模块热插拔,但依赖图复杂易出环 |
Tomcat 的 WebappClassLoader 就是典型:每个 Web 应用一个加载器,WEB-INF/classes 与 WEB-INF/lib 优先,javax.servlet 等容器 API 则委派给共享加载器,这样两个应用可以跑不同版本的 Spring。
四、TCCL 的代码形态
// 父加载器加载的框架代码,需要拿到子加载器里的实现类
public class SpiLoader {
public static <T> T load(Class<T> api) throws Exception {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Class<?> impl = Class.forName("com.example.MyImpl", true, tccl);
return api.cast(impl.getDeclaredConstructor().newInstance());
}
public static void main(String[] args) throws Exception {
Thread.currentThread().setContextClassLoader(
new java.net.URLClassLoader(new java.net.URL[]{
new java.io.File("plugins/impl.jar").toURI().toURL()
}));
Runnable r = load(Runnable.class);
r.run();
}
}
DriverManager 之所以能加载各厂商驱动,靠的就是启动类加载器加载的 DriverManager 通过 TCCL 去扫 META-INF/services/java.sql.Driver。
五、JDK 9 模块化后的变化
| 变化点 | JDK 8 | JDK 9+ |
|---|---|---|
| 扩展类加载器 | ExtClassLoader | 改为 PlatformClassLoader |
| 启动类加载器路径 | rt.jar | jrt:/ 模块系统 |
| 委派顺序 | 严格父优先 | 先父后子,但模块内类优先由定义模块的加载器加载 |
| 核心包保护 | preDefineClass 检查 java. 前缀 | 同上,并增加模块封装(--add-opens 才可反射) |
六、常见追问
| 追问 | 回答要点 |
|---|---|
| 双亲委派能防止什么 | 防止核心类被同名类覆盖(安全)、防止同一个类被重复加载(一致性) |
| 怎么自己写一个绕过委派的加载器 | 重写 loadClass,去掉 parent.loadClass 直接 findClass;但 java.* 包会被 preDefineClass 拒绝 |
| Class.forName 用哪个加载器 | 默认用调用方的加载器,可显式传 ClassLoader;这也是 TCCL 存在的理由 |
| Spring Boot 为什么能一个 JAR 起服务 | LaunchedURLClassLoader 从嵌套 JAR 读字节流,同时保留父委派加载 JDK 类 |
| 破坏双亲委派有风险吗 | 有,可能出现同一个类被两个加载器各加载一份,instanceof 与强转失败,排查困难 |
小结
双亲委派是「父优先」的默认约定,保障核心类唯一;破坏它的动机都是「父加载器看不到子加载器的东西」或「需要版本隔离」。答题时给出委派链路 + 三条破坏路径 + 一个真实案例,就够了。