一、需要破坏的场景:
1.基础类需要调用用户的代码
矛盾点:双亲委派中,核心类(由Bootstrap ClassLoader加载的)无法看到应用类加载器路径下的驱动实现类(如:MySQL驱动)
需求:核心类库must能够反向加载第三方厂商实现的代码
2.同一个应用服务器need隔离不同的web应用
矛盾点:两个web应用同时依赖了同一个开源库(eg:Spring),但一个时spring 4,一个是spring 5,如果走双亲委派,全局只会加载其中一个,导致另一个崩溃
需求:每个Web应用must有独立的类加载器,优先加载自己目录下的类,实现版本隔离
3.热部署与热修复
矛盾点:双亲委派中,类一旦加载就无法轻易卸载或更替
需求:需要动态替换某个模块,当模块更新时,旧的类加载器丢弃,创建全新的
二、破坏方式:
1.线程上下文类加载器(Context ClassLoader)
解决 “核心类需要调用用户代码” 的场景(JDBC、JNDI)
- 原理:利用
Thread.currentThread().getContextClassLoader()。这个上下文类加载器默认是AppClassLoader - 可以通过这个线程上下文加载器去加载所需的SPI代码
JDBC:
像JDBC这样,接口定义在Java核心库中,由BootstrapClassLoader加载,但是具体的数据库驱动实现是第三方提供的(我们在项目中引入的maven依赖),只能由AppClassLoder加载
此时双亲委派走不通,靠的就是线程上下文加载器:
流程:
当在yml里配置了MySql驱动,
Spring Boot 启动,读取配置,并触发 DriverManager 的初始化
但是核心库里根本找不到这个类
方法内部调用了load方法去获取线程上下文类加载器
//官方源码
package java.sql;
private static final String JDBC_DRIVERS_PROPERTY = "jdbc.drivers";
@SuppressWarnings("removal")
private static void ensureDriversInitialized() {
.....
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
//这里去找
return System.getProperty(JDBC_DRIVERS_PROPERTY);
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//===获取线程上下文类加载器===
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
.....
}
利用 `Thread.currentThread().getContextClassLoader()`。这个上下文类加载器默认是 AppClassLoader
//官方源码
package java.util;
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取【当前线程的上下文类加载器】
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//传给 ServiceLoader 内部,让它用这个 cl 去 ClassPath 里捞驱动实现类
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
线程上下文类加载器就会去找这个类
//maven依赖
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
//把自己new出来,然后扔给 DriverManager
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
这里的registerDriver,是 java.sql 包下的
public static void registerDriver(java.sql.Driver driver)
throws SQLException {
//实际上是调用了第二个,只不过不传入清理动作(null)
registerDriver(driver, null);
}
public static void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if (driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
∵ public class Driver extends NonRegisteringDriver implements java.sql.Driver
所以这个类实现了Driver接口,重写了里面定的规范,这里可以直接接收所有实现了这个接口的类,核心就是====>多态

那为什么又有两个registerDriver方法?
主要是功能升级,因为驱动光“注册”还不够,当驱动被注销或者DriverManager关闭时,有些驱动可能还需要做一些清理工作(eg:释放连接、关闭线程池)所以引入了第二个参数 DriverAction da(这也是一个接口),用来专门回调驱动被销毁时的后续动作。
所以JDK为了能连接上jdbc自动使用线程上下文类加载器去破坏了双亲委派。
这里都破坏了双亲委派机制那这个机制还有什么作用呢?这种破坏是短时间的破坏又迅速恢复了,还是说这是局部的只有这里的JDBC能用?
它既是局部的,也是“瞬时/临时”的。
1.是局部的(只在特定场景作为“后门”使用)
双亲委派模型并没有因为 JDBC 倒塌。这属于一种典型的“父类加载器请求子类加载器”的特殊场景。Java 只有在类似 SPI 机制(如 JDBC、JNDI、JAXP 等规范) 这种核心库需要调用第三方实现时,才会动用这个后门。普通的类加载(比如写一个 UserService 引入 OrderService)依然严格死守双亲委派模型。
2.它在代码执行时是“瞬时”的
在调用 ServiceLoader.load() 的那一刻,利用 Thread.currentThread().getContextClassLoader() 获取到子加载器把类装载进来。类一旦装载完毕、注册成功,这个临时“向下寻找”的动作就结束了。
而且,这个上下文加载器不需要“迅速恢复”,因为线程上下文类加载器本来就是长驻在线程身上的一个属性(默认就是 Application ClassLoader)。JDK 只是在需要的时候主动去读取了它一下,并没有修改这个加载器的结构。
3.这里都破坏了,那双亲委派机制还有什么作用呢?
“既然能开后门,那双亲委派是不是名存实亡了?”
完全不是。双亲委派的核心历史使命是:保护 JDK 核心类库不被污染、不被篡改
它依然有用:
假设有个黑客不怀好意,在自己的 Maven 依赖里写了一个假冒的 java.lang.String 类,企图在里面偷取用户的密码。
当程序运行,需要加载 java.lang.String 时:
- 依然会走正统的双亲委派流程:一路向上委派给最顶层的 启动类加载器(Bootstrap ClassLoader)。
- 启动类加载器一看:“哦,你要加载
java.lang.String啊,我本地的rt.jar(或 Java 模块)里有官方正版的。” - 启动类加载器直接把正版的
java.lang.String加载返回。黑客写的那个同名假类永远没有机会被加载。
难道除了启动类加载器,其他的都要靠破坏双亲委派才能执行其中的逻辑吗?双亲委派的机制不就是层层递交吗?
这里的JDBC 是一个极少数的、特例中的特例。
99% 的场景:
在日常开发中,不管是自己写的 UserService,还是引入的第三方依赖(比如 Spring、MyBatis),它们的加载全部都在严格遵守双亲委派。
假设代码里写了一行:UserService service = new UserService();
此时,负责加载业务代码的 应用类加载器(Application ClassLoader) 收到任务,它开始标准的“层层递交”:
-
第一步(向上递交):应用类加载器不直接加载,而是递交给它的父亲——扩展类加载器。
-
第二步(继续向上):扩展类加载器也不直接加载,而是递交给它的父亲——启动类加载器。
-
第三步(向下反馈):
- 启动类加载器 在核心库里找了找,摇摇头:“这不是我管的(没找到)”,把它退回给扩展类加载器。
- 扩展类加载器 在扩展库里找了找,也摇摇头:“这也不是我管的”,把它退回给应用类加载器。
-
第四步(自行加载):应用类加载器 接过来一看:“哦,这是在我们自己的项目路径(Classpath)下的类”,于是自己出手把
UserService加载进了内存。
在这个全过程中,扩展类加载器和应用类加载器都执行了自己的逻辑,发挥了各自的作用。整个过程行云流水,没有任何人破坏双亲委派。
那么问题来了:为什么只有 JDBC(SPI)成为了“破坏者”?
关键的区别在于:到底是谁在“主动”发起加载的请求。
- 普通场景(不破坏):发起请求的是底层(业务代码)。写的代码调用别的类,可以“自下而上”层层递交去请示老祖宗,这很符合规矩。
- JDBC 场景(必破坏):发起请求的是顶层(JDK 官方核心库里的
DriverManager)。老祖宗(启动类加载器)在执行初始化时,突然想调用底层的具体实现(MySQL 驱动)。但由于双亲委派规定了“只能下级请示上级,不能上级俯视下级”,老祖宗没办法“自上而下”去 Classpath 里找类。
好比:
普通场景(遵守规矩):公司底层员工(应用类加载器)遇到不懂的专业术语,写了一份报告层层上报给董事长(启动类加载器)。董事长说这不归我管,逐层批复退回,最后员工自己查字典解决了。这叫双亲委派。
JDBC 场景(被迫破坏):董事长(JDK 核心库)在制定公司战略(JDBC 规范)时,突然需要看一份藏在底层员工电脑里的 MySQL 数据报表。但董事长按照公司严格的官僚体制(双亲委派),没办法直接向下调取底层员工的电脑文件。于是董事长只能用特权(线程上下文类加载器)开个后门,直接伸手去底层员工的电脑里把报表拿了过来。
2.重写 loadClass() 方法
最直接的硬破坏 , 经典代表是 Tomcat的WebappClassLoader
注意:Tomcat 依然不敢去加载 java.lang.String 这种核心类。在 Tomcat 的 loadClass 源码中,第一步依然是交由 BootstrapClassLoader 加载核心类,防止核心类被污染。它破坏的只是 Application ClassLoader 这一层的顺序(把“父类优先”改成了“子类优先”)。
3.网状结构的模块化(eg:OSGI)
OSGi(Open Services Gateway initiative)将类加载器发展成了网状结构,而不再是树状结构
- 原理:在 OSGi 内部,每个 Bundle(模块)都有自己独立的类加载器。
- 破坏点:当一个 Bundle 需要加载类时,它不是盲目向上找父类,而是根据配置的导包规则(Import-Package),平级地去委派给动态提供该类的那个 Bundle。这种极其复杂的动态借调,彻底打破了传统双亲委派的线性结构。

1753

被折叠的 条评论
为什么被折叠?



