Java打破双亲委派案例

wen java案例 3

本文目录导读:

Java打破双亲委派案例

  1. 双亲委派模型的常规流程(回顾)
  2. 经典案例:Tomcat 的 WebAppClassLoader
  3. 具体场景案例:为什么打破双亲委派能解决冲突?
  4. 手写一个最小的“打破双亲委派” Demo
  5. 另一个著名案例:JDK 9+ 的模块系统(JPMS)与 SPI
  6. 打破双亲委派的风险与注意事项

在 Java 中,打破双亲委派模型(Breaking Parent-Delegate Model)是许多框架(如 Tomcat、Spring Boot、OSGi、热部署工具)实现类隔离、热替换、Lib 冲突解决的核心机制。

最常见的打破双亲委派案例是 Tomcat 的 WebAppClassLoader

下面我将从 原理、核心代码、实际案例(Tomcat)、以及一个手写的最小 Demo 来详细讲解。


双亲委派模型的常规流程(回顾)

正常情况下,ClassLoader.loadClass() 的逻辑是:

  1. 检查类是否已加载。
  2. 若未加载,先委托给父加载器(Parent)尝试加载。
  3. 若父加载器加载失败,才由自身(当前加载器)尝试加载。

打破的关键在于:重写 loadClass 方法,改变“先父后子”的顺序,改为“先子后父”或“优先从特定路径加载”。


经典案例:Tomcat 的 WebAppClassLoader

背景:一个 Tomcat 容器部署了多个 Web 应用(WAR包),每个应用可能使用了不同版本的同一个第三方库(Spring 4 和 Spring 5),如果所有应用都委托给同一个 Common ClassLoader,则只能加载一个版本,另一个会报 NoSuchMethodError

Tomcat 的解决方案: 它定义了一个 WebappClassLoader,这个加载器打破了双亲委派,其加载顺序是:

  1. 先自己尝试加载(从 /WEB-INF/classes/WEB-INF/lib 中加载)。
  2. 如果没有找到,再委派给父加载器(JVM 的 AppClassLoader 或 ExtClassLoader)。

为什么不完全依赖父加载器? 因为父加载器(Common ClassLoader)加载的是 Tomcat 全局共享的类,而 Web 应用自己的类必须隔离,如果父加载器已经加载了 Foo.class,子加载器永远不会加载 Foo.class


Tomcat 核心逻辑代码(简化示意)

Tomcat 的 WebappClassLoaderBase 重写了 loadClass 方法,核心逻辑如下:

// 简化自 Tomcat 源码 (WebappClassLoaderBase.java)
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 1. 检查是否已经加载过(JVM 内部缓存)
    Class<?> clazz = findLoadedClass(name);
    if (clazz == null) {
        // 2. (打破双亲委派的关键) 先尝试自己加载(从WEB-INF/classes和WEB-INF/lib)
        try {
            clazz = findClass(name);
        } catch (ClassNotFoundException e) {
            // 3. 自己加载失败,才委派给父加载器
            if (clazz == null) {
                try {
                    clazz = super.loadClass(name, false); // 这里会走父加载器逻辑
                } catch (ClassNotFoundException e2) {
                    throw e2;
                }
            }
        }
    }
    if (resolve) {
        resolveClass(clazz);
    }
    return clazz;
}
// 关键:findClass 如何找到类? 查找 WEB-INF/classes 目录和 WEB-INF/lib 下的 JAR。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 1. 从 /WEB-INF/classes 目录找 .class 文件
    // 2. 从 /WEB-INF/lib 下的 jar 中找 .class 文件
    // 找到后使用 defineClass 生成 Class 对象
}

注释:这只是一种简化模式,Tomcat 对系统类库(如 `java.`)依然强制委托给 Bootstrap,防止破坏 JDK 核心类。*


具体场景案例:为什么打破双亲委派能解决冲突?

假设有两个应用 AppAAppB

  • AppA 使用了 MyLib v1.0 (包含 com.demo.Util)
  • AppB 使用了 MyLib v2.0 (包含 com.demo.Util,但方法签名不同)

如果不打破双亲委派: 系统只有一个 AppClassLoader,它会加载第一个找到的 Util 类(v1.0),当 AppB 调用 Util 的新方法时,会报 NoSuchMethodError

Tomcat 打破后:

  • AppAWebAppClassLoaderA 在自己的 WEB-INF 中找到了 Util(v1.0),将其加载。
  • AppBWebAppClassLoaderB 在自己的 WEB-INF 中找到了 Util(v2.0),将其加载。
  • 两个类在 JVM 中完全独立,互不干扰。
  • AppB 找不到时,才会往上找父加载器,但通常 AppB 能找到自己的。

手写一个最小的“打破双亲委派” Demo

为了直观演示,我们写一个最简单的自定义类加载器,它重写 loadClass 方法,强制要求优先自己加载(模拟热部署/类隔离场景)。

前提:为了演示,我们需要一个不在 classpath 中的类文件,或者直接生成字节码,这里我们可以创建一个 Hello.class 放在某个磁盘路径下。

代码演示:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class BreakingDelegationDemo {
    // 模拟一个存在于磁盘上的类文件(不在 JVM 的 classpath 中)
    // 假设 /tmp/classes/com/demo/Hello.class 存在
    // 这里我们直接用字符串作为类的字节码来源(如果是复杂情况,可以读取文件)
    public static void main(String[] args) throws Exception {
        // 1. 自定义一个打破双亲委派的类加载器
        ClassLoader customLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                // 1. 安全检查:如果是 JDK 核心类,必须委托给系统(不能打破)
                if (name.startsWith("java.")) {
                    return super.loadClass(name);
                }
                // 2. 打破双亲委派:"自己先去磁盘找"
                System.out.println("[CustomLoader] 尝试自己加载: " + name);
                String fileName = "/tmp/classes/" + name.replace('.', '/') + ".class";
                try {
                    byte[] bytes = readFile(fileName);
                    if (bytes != null) {
                        // 使用 defineClass 将字节码转为 Class 对象
                        return defineClass(name, bytes, 0, bytes.length);
                    }
                } catch (IOException e) {
                    // 忽略,去父加载器找
                }
                // 3. 自己没找到,调用父加载器(这里体现“委派”)
                System.out.println("[CustomLoader] 自己未找到,委派给父: " + name);
                return super.loadClass(name);
            }
        };
        // 2. 准备一个目标类(比如直接加载一个已有的类,看它是被谁加载的)
        // 注意:Hello 类在 classpath 里,父加载器能找到它;
        // 但因为我们破了规则,会先尝试从 /tmp/classes 找,找不到才用父加载器。
        Class<?> clazz = customLoader.loadClass("com.demo.Hello");
        System.out.println("加载成功: " + clazz.getClassLoader());
        // 3. 对比:如果用系统类加载器加载,看看它是否不同
        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
        Class<?> clazzSys = systemLoader.loadClass("com.demo.Hello");
        System.out.println("系统加载成功: " + clazzSys.getClassLoader());
        // 验证:两个类是否相同(类名相同但类加载器不同,则视为不同的类)
        System.out.println("是否为同一个类对象: " + (clazz == clazzSys));
    }
    private static byte[] readFile(String path) throws IOException {
        File file = new File(path);
        if (!file.exists()) {
            return null;
        }
        try (FileInputStream fis = new FileInputStream(file)) {
            return fis.readAllBytes();
        }
    }
}

运行结果分析:

  • 如果你把 com.demo.Hello 编译并放到 /tmp/classes 下,CustomLoader优先加载它(即使它也在 classpath 中),且打印“尝试自己加载”。
  • 如果没有放在 /tmp/classes,则会委派给父加载器,从 classpath 中加载。

另一个著名案例:JDK 9+ 的模块系统(JPMS)与 SPI

在 JDK 9 之前,ClassLoader.getSystemClassLoader() 会打破双亲委派的一个经典例子是 JDBC / SPI(Service Provider Interface),DriverManager 加载 JDBC Driver 时,因为 DriverManager 是 Bootstrap 类加载器加载的,而 JDBC Driver 在 Application Classpath,Bootstrap 无法看不见它们,导致无法加载,这就有了 Thread.currentThread().getContextClassLoader() 的诞生,它在“父加载器”环境中调用“子加载器”的上下文加载器来加载实现类。


打破双亲委派的风险与注意事项

优点 缺点/风险
类隔离:不同模块使用不同版本的库互不干扰 类重复加载:同一份类被多个加载器加载,浪费内存
热部署:重新加载类(替换 JAR 即可) instanceof 相等性问题A.class.isInstance(B) 可能为 false,因为类比较不仅看类名,还看类加载器
框架扩展:如 Web 容器 / OSGi 容易破坏类型一致性,导致 ClassCastException
实现 SPI 扩展 需手动处理资源、类路径、安全策略

核心代码总结: 打破双亲委派 = 重写 ClassLoader.loadClass(String, boolean) 方法,将原本的 parent.loadClass() 逻辑延后,自己的 findClass() 逻辑提前

这样,Java 生态中的 Web 容器(Tomcat、Jetty)、OSGi(Eclipse)、热部署工具(JRebel、Arthas 等)才能高效稳定地工作。

抱歉,评论功能暂时关闭!