打破双亲委派模型

打破双亲委派模型通过重写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)

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

相关推荐
橘猫云计算机设计5 小时前
基于Springboot的自习室预约系统的设计与实现(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·毕业设计
秋书一叶6 小时前
SpringBoot项目打包为window安装包
java·spring boot·后端
pwzs6 小时前
Spring MVC 执行流程全解析:从请求到响应的七步走
java·后端·spring·spring mvc
小兵张健6 小时前
互联网必备职场知识(4)—— 共情沟通能力
后端·产品经理·运营
AskHarries7 小时前
使用 acme.sh 自动更新 SSL 证书的指南
后端
Chandler248 小时前
Go:反射
开发语言·后端·golang
pwzs8 小时前
深入浅出 MVCC:MySQL 并发背后的多版本世界
数据库·后端·mysql
盒子69108 小时前
go for 闭环问题【踩坑记录】
开发语言·后端·golang
刘大猫269 小时前
Arthas monitor(方法执行监控)
人工智能·后端·监控
追逐时光者9 小时前
MongoDB从入门到实战之MongoDB简介
后端·mongodb