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 时:

  1. 依然会走正统的双亲委派流程:一路向上委派给最顶层的 启动类加载器(Bootstrap ClassLoader)
  2. 启动类加载器一看:“哦,你要加载 java.lang.String 啊,我本地的 rt.jar(或 Java 模块)里有官方正版的。”
  3. 启动类加载器直接把正版的 java.lang.String 加载返回。黑客写的那个同名假类永远没有机会被加载

难道除了启动类加载器,其他的都要靠破坏双亲委派才能执行其中的逻辑吗?双亲委派的机制不就是层层递交吗?

这里的JDBC 是一个极少数的、特例中的特例

99% 的场景:

在日常开发中,不管是自己写的 UserService,还是引入的第三方依赖(比如 Spring、MyBatis),它们的加载全部都在严格遵守双亲委派

假设代码里写了一行:UserService service = new UserService();

此时,负责加载业务代码的 应用类加载器(Application ClassLoader) 收到任务,它开始标准的“层层递交”:

  1. 第一步(向上递交):应用类加载器不直接加载,而是递交给它的父亲——扩展类加载器

  2. 第二步(继续向上):扩展类加载器也不直接加载,而是递交给它的父亲——启动类加载器

  3. 第三步(向下反馈)

    • 启动类加载器 在核心库里找了找,摇摇头:“这不是我管的(没找到)”,把它退回给扩展类加载器。
    • 扩展类加载器 在扩展库里找了找,也摇摇头:“这也不是我管的”,把它退回给应用类加载器。
  4. 第四步(自行加载)应用类加载器 接过来一看:“哦,这是在我们自己的项目路径(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。这种极其复杂的动态借调,彻底打破了传统双亲委派的线性结构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值