欢迎光临
我们一直在努力

JVM===>“破坏双亲委派机制”<===场景与源码分析

一、需要破坏的场景:

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。这种极其复杂的动态借调,彻底打破了传统双亲委派的线性结构。
    赞(0)
    未经允许不得转载:171主机测评 » JVM===>“破坏双亲委派机制”<===场景与源码分析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址