打破双亲委派模型

打破双亲委派模型通过重写loadClass()、线程上下文类加载器及OSGi机制实现类隔离、SPI驱动加载与模块化,应用于Tomcat、JDBC和热部署场景,解决核心类访问与动态加载需求。

一、为什么需要打破双亲委派?

双亲委派模型的局限性:

  1. 父加载器无法访问子加载器的类 : 例如,JDBC 的 DriverManager(由启动类加载器加载)需要加载第三方数据库驱动(由应用类加载器加载),但父加载器无法直接访问子加载器的类。
  2. 类隔离需求: 如 Tomcat 需要隔离不同 Web 应用的类,避免同名类冲突。
  3. 动态模块化加载: OSGi 框架需要支持模块的动态安装、卸载和热替换。

二、打破双亲委派的实现方式

方式 1:重写 loadClass() 方法(改变委派逻辑)

默认的 loadClass() 方法遵循双亲委派,通过重写该方法可以调整加载顺序。

示例:Tomcat 的 WebAppClassLoader

scala 复制代码
public class WebAppClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 1. 检查是否已加载
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }
​
        // 2. 优先加载本地类(破坏双亲委派)
        if (isWebAppClass(name)) {
            try {
                return findClass(name); // 自行加载
            } catch (ClassNotFoundException e) {
                // 忽略异常,继续委派父加载器
            }
        }
​
        // 3. 委派父加载器加载(若未找到,继续向上委派)
        return super.loadClass(name, resolve);
    }
​
    private boolean isWebAppClass(String className) {
        // 判断是否为当前 Web 应用的类(如检查包路径)
        return className.startsWith("com.mywebapp.");
    }
}

逻辑说明

  • Tomcat 的 WebAppClassLoader 优先加载自身应用目录下的类,而不是直接委派给父加载器。
  • 只有无法加载本地类时,才委派给父加载器,从而实现不同 Web 应用的类隔离。

方式 2:使用线程上下文类加载器(Context ClassLoader)

通过设置当前线程的上下文类加载器,父类加载器可以绕过双亲委派,直接使用子类加载器加载资源。

示例:JDBC 的 SPI 机制 JDBC 的 DriverManager 由启动类加载器加载,但数据库驱动(如 com.mysql.jdbc.Driver)由应用类加载器加载。为了解决父加载器无法访问子加载器类的问题,JDBC 使用线程上下文类加载器:

typescript 复制代码
// JDBC 获取驱动的核心代码(简化版)
public class DriverManager {
    static {
        // 通过上下文类加载器加载驱动
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        while (driversIterator.hasNext()) {
            driversIterator.next();
        }
    }
​
    public static Connection getConnection(String url) {
        // ...
    }
}
​
// ServiceLoader.load() 内部逻辑:
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return new ServiceLoader<>(service, cl);
}

关键步骤

  1. 在调用 DriverManager.getConnection() 前,应用代码需设置线程上下文类加载器:

    scss 复制代码
    Thread.currentThread().setContextClassLoader(myClassLoader);
  2. DriverManager 使用该上下文类加载器加载第三方驱动。


方式 3:OSGi 的类加载模型

OSGi 的每个模块(Bundle)使用独立的类加载器,通过复杂的委派规则实现动态模块化。

OSGi 类加载规则

  1. 委派给父 Bundle 的类加载器: 若类在依赖的父 Bundle 中定义,优先委派父 Bundle 的加载器。
  2. 委派给 Import-Package 的 Bundle : 若类属于导入的包(Import-Package),委派给导出该包的 Bundle 的加载器。
  3. 查找本地 Bundle 类路径: 若未找到,尝试从当前 Bundle 的类路径加载。
  4. 委派给动态代理类加载器: 若需要生成动态代理类,使用特定的代理类加载器。

效果

  • 支持模块热插拔和版本隔离。
  • 不同 Bundle 可以加载同一类的不同版本,互不影响。

三、自定义类加载器示例(热部署场景)

以下是一个自定义类加载器,通过 打破双亲委派 实现热部署(重新加载修改后的类):

java 复制代码
public class HotDeployClassLoader extends ClassLoader {
    private String classPath; // 类加载路径
​
    public HotDeployClassLoader(String classPath) {
        this.classPath = classPath;
    }
​
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查是否已加载
            Class<?> loadedClass = findLoadedClass(name);
            if (loadedClass != null) {
                return loadedClass;
            }
​
            // 2. 优先加载指定路径下的类(打破双亲委派)
            if (name.startsWith("com.example.hotdeploy")) {
                try {
                    return findClass(name);
                } catch (ClassNotFoundException e) {
                    // 忽略,继续委派父加载器
                }
            }
​
            // 3. 其他类仍走双亲委派
            return super.loadClass(name, resolve);
        }
    }
​
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 从指定路径加载字节码
        byte[] bytes = loadClassBytes(name);
        return defineClass(name, bytes, 0, bytes.length);
    }
​
    private byte[] loadClassBytes(String className) {
        // 实现从文件系统或网络加载字节码的逻辑
        String path = classPath + className.replace('.', '/') + ".class";
        try (InputStream is = new FileInputStream(path);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException("加载类失败: " + className, e);
        }
    }
}

使用方式

ini 复制代码
// 1. 创建自定义加载器实例
HotDeployClassLoader loader = new HotDeployClassLoader("/path/to/classes/");
​
// 2. 加载类(优先从指定路径加载)
Class<?> clazz = loader.loadClass("com.example.hotdeploy.MyService");
​
// 3. 修改 MyService 类后,重新创建加载器实例即可生效
HotDeployClassLoader newLoader = new HotDeployClassLoader("/path/to/classes/");
Class<?> newClazz = newLoader.loadClass("com.example.hotdeploy.MyService");

效果

  • 修改 MyService 类后,通过新建类加载器实例加载新类,旧类可被 GC 回收,实现热部署。

四、注意事项

  1. 类唯一性问题 : 同一类由不同加载器加载会被视为不同类,导致 instanceof 和类型转换失败。
  2. 资源泄漏: 自定义类加载器需谨慎管理,避免因长期持有引用导致类无法卸载。
  3. 安全性: 避免加载不可信的字节码,防止恶意代码注入。

五、总结

打破双亲委派的场景与实现:

场景 实现方式 典型案例
类隔离 重写 loadClass() 优先自行加载 Tomcat 的 WebAppClassLoader
父加载器访问子加载器 设置线程上下文类加载器 JDBC 的 SPI 机制
动态模块化 独立的类加载器与复杂委派规则 OSGi 框架
热部署 每次重新创建类加载器实例 热部署工具(JRebel)

理解如何打破双亲委派,是掌握复杂类加载场景(如中间件开发、模块化架构)的关键能力。

相关推荐
佚名涙2 小时前
go中锁的入门到进阶使用
开发语言·后端·golang
草捏子7 小时前
从CPU原理看:为什么你的代码会让CPU"原地爆炸"?
后端·cpu
嘟嘟MD8 小时前
程序员副业 | 2025年3月复盘
后端·创业
胡图蛋.8 小时前
Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个?
java·spring boot·后端
无责任此方_修行中8 小时前
关于 Node.js 原生支持 TypeScript 的总结
后端·typescript·node.js
吃海鲜的骆驼9 小时前
SpringBoot详细教程(持续更新中...)
java·spring boot·后端
迷雾骑士9 小时前
SpringBoot中WebMvcConfigurer注册多个拦截器(addInterceptors)时的顺序问题(二)
java·spring boot·后端·interceptor
uhakadotcom10 小时前
Thrift2: HBase 多语言访问的利器
后端·面试·github
Asthenia041210 小时前
Java 类加载规则深度解析:从双亲委派到 JDBC 与 Tomcat 的突破
后端
方圆想当图灵10 小时前
从 Java 到 Go:面向对象的巨人与云原生的轻骑兵
后端·代码规范