颠覆认知:Java 打破双亲委派 ≠ 彻底废弃双亲委派模型

前言

上个月有位朋友去面试,面试官先问了什么是打破双亲委派机制,他只能照着背八股理论。紧接着面试官追问:打破这个机制到底有什么实际作用?

查阅资料后总结:双亲委派是 Java 默认的类加载规则,而打破该机制主要分为两类场景:

  1. 业务开发场景 :通过自定义类加载器绕开默认委派逻辑,可实现插件化、运行时热更新类逻辑、线上热修复等能力;需注意:仅打破委派无法替换已加载类,必须新建类加载器重新加载字节码才能完成更新。
  2. JDK / 中间件原生场景 :JDK 的 SPI 机制(如 JDBC)、Tomcat 等 Web 容器,也主动打破了双亲委派,目的是解决类加载层级限制、实现多应用类隔离与 Jar 包版本兼容。

顺着这个思路,下面我们就一步步深入,设计打破双亲委派的场景。


一、类加载器实现类


二、打破双亲委派

打破双亲委派 ≠ 完全不用双亲委派机制

1、实现流程逻辑

  1. 重写 loadClass() 方法
  2. 对于 自定义的com.nl.xx.* 包下的类不调用 super.loadClass()
  3. 不委托给父类,直接自己处理,直接调用自己的 findClass() 方法
  4. 实现自定义的类查找和加载逻辑

2、🌰举个例子

2.1、自定义类加载器

java 复制代码
package com.nl.classloader.custom;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.SimpleRemapper;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * 核心:当加载 targetClass 时,实际上用 sourceClass 的字节码替换它(通过 ASM 技术修改类名)
 */
public class ReplaceClassLoader extends ClassLoader {
    private final String targetClass;
    private final String sourceClass;
    private final Path appRoot, jarRoot;
    private final String targetPackage;

    /**
     * @param appRoot 应用程序 class 文件的根目录(存放普通类)
     * @param jarRoot JAR 包中 class 文件的根目录(存放 sourceClass 的类)
     * @param targetClass 目标类名(要被替换的类,如 CustomTestDefault)
     * @param sourceClass 源类名(实际使用的类,如 CustomTestV1)
     */
    public ReplaceClassLoader(String appRoot, String jarRoot, String targetClass, String sourceClass) {
        this.appRoot = Paths.get(appRoot);
        this.jarRoot = Paths.get(jarRoot);
        this.targetClass = targetClass;
        this.sourceClass = sourceClass;
        // 动态提取包名
        this.targetPackage = targetClass.substring(0, targetClass.lastIndexOf('.') + 1);
    }

    /**
     * 加载类的方法
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 自定义包前缀时,使用自定义的类加载
        if (name.startsWith(targetPackage)) {
            // 为每个类名提供独立的锁
            synchronized (getClassLoadingLock(name)) {
                // 检查 JVM 是否已经加载过这个类
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    // 直接调用 findClass(name),不委托给父类加载器
                    // 这就是"打破双亲委派"的关键!
                    c = findClass(name);
                    if (resolve) resolveClass(c);
                }
                return c;
            }
        }
        return super.loadClass(name, resolve);
    }

    /**
     * 使用ASM 改名,原理:
     * SimpleRemapper: 简单的类名映射器,将所有出现的 sourceClass 类名替换为 targetClass
     * ClassRemapper: 遍历整个字节码,应用映射规则
     * 不仅改类名,还会修改所有引用这个类的地方(如方法签名、字段类型等)
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = null;
            if (targetClass.equals(name)) {
                // 步骤1:从 jarRoot 读取 sourceClass 的 class 文件
                ClassReader cr = new ClassReader(Files.readAllBytes(jarRoot.resolve(sourceClass.replace('.', '/') + ".class")));
                // 步骤2:创建 ClassWriter,自动计算栈帧和最大局部变量
                ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
                // 步骤3:使用 ClassRemapper 将 sourceClass 改名为 targetClass
                cr.accept(new ClassRemapper(cw, new SimpleRemapper(sourceClass.replace('.', '/'), targetClass.replace('.', '/'))), ClassReader.EXPAND_FRAMES);
                // 步骤4:生成修改后的字节码
                bytes = cw.toByteArray();
            } else {
                bytes = Files.readAllBytes(appRoot.resolve(name.replace('.', '/') + ".class"));
            }
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException(name, e);
        }
    }
}

2.2、默认CustomTestDefault

csharp 复制代码
package com.nl.classloader.custom;

public class CustomTestDefault {
    public void say() {
        System.out.println("CustomTestDefault:hello world");
    }
}

2.3、替换CustomTestV1

csharp 复制代码
package com.nl.classloader.custom;

public class CustomTestV1 {
    public void say() {
        System.out.println("CustomTestV1:hello world");
    }
}

2.4、执行

java 复制代码
package com.nl.classloader.custom;

public class ClassLoaderLauncher {

    public static void main(String[] args) throws Exception {

        // 直接new(仍使用系统类加载器,不会替换)
        CustomTestDefault customTestDefault = new CustomTestDefault();
        customTestDefault.say();

        ReplaceClassLoader replaceClassLoader = new ReplaceClassLoader(
                "nl-class/target/classes/",
                "nl-class-jar/target/classes/",
                "com.nl.classloader.custom.CustomTestDefault",
                "com.nl.classloader.custom.CustomTestV1"
        );

        // 通过自定义类加载器反射调用
        Class<?> demoClass = replaceClassLoader.loadClass("com.nl.classloader.custom.CustomTestDefault");
        Object demo = demoClass.getDeclaredConstructor().newInstance();
        demoClass.getMethod("say").invoke(demo);

    }
}

2.5、输出结果

makefile 复制代码
CustomTestDefault:hello world
CustomTestV1:hello world

⚠️注意

✔️示例中两次实例化的均为 CustomTestDefault 类,但输出结果截然不同,核心原因是类加载过程中该类的字节码被替换了

3、完整的类加载流程对比

3.1、正常流程

scss 复制代码
new CustomDemoService()
    ↓
AppClassLoader.loadClass()
    ↓
ExtensionClassLoader.loadClass()
    ↓
BootstrapClassLoader.loadClass()
    ↓
找不到,逐层返回
    ↓
ExtensionClassLoader.findClass() - 找不到
    ↓
AppClassLoader.findClass() - 找到并加载

3.2、打破双亲委派

scss 复制代码
loader.loadClass("CustomDemoService")  ← ReplaceClassLoader
    ↓
判断:属于 CUSTOM_PKG?是
    ↓
不调用 parent.loadClass(),直接 findClass()
    ↓
ReplaceClassLoader.findClass()
    ↓
从文件系统读取 .class 文件
    ↓
如果是 ClassLoaderCustomTestDefault
    ↓
用 V2 字节码替换,ASM 改名
    ↓
defineClass() 定义类

三、面试题

1、哪些 Jar 包适合采用外部加载?

整体执行流程固定、具体业务规则迭代频繁的模块,可独立作为外部 Jar 加载。典型场景包括规则引擎、统一审批逻辑、订单状态流转规则。

2、外部jar包可以放到哪些地方?

外部 Jar 包支持本地存储和远程加载两种方式:

  • 本地:服务器指定磁盘目录、应用自定义文件夹;
  • 远程 :通过 URLClassLoader 从 Web 服务器远程加载 Jar 包;像 Drools 规则引擎还支持从 Maven 仓库远程加载规则文件与依赖包。

四、总结

打破双亲委派 ≠ 完全不用双亲委派机制

而是:

  • 重写 loadClass() 方法
  • 在特定条件下不调用 super.loadClass()
  • 直接调用自己的 findClass() 方法
  • 实现自定义的类查找和加载逻辑

这是一种策略性的绕过,让你可以在某些场景下控制类的加载过程,比如:

  • 热部署(重新加载已存在的类)
  • 隔离不同模块的类版本
  • AOP 字节码增强
  • 你的场景:透明替换类的实现
相关推荐
春天花会开1311 小时前
影像上传前置机网络架构设计模板(含VPN)
后端·架构
ZengLiangYi1 小时前
5 种 AI 对话数据格式全解析
后端·aigc·ai编程
ZengLiangYi1 小时前
本地向量数据库选型:vectra vs chroma vs hnswlib
javascript·数据库·后端
咖啡八杯1 小时前
【无标题】
java·后端·设计模式
Rain5093 小时前
1.3. Next.js与Nest.js在AI数据分析中的角色
前端·javascript·人工智能·后端·数据分析·node.js·ai编程
techdashen3 小时前
Rust 项目进展月报:2026 年 1 月
开发语言·后端·rust
武子康3 小时前
调查研究-170 Vert.x 是什么?它和 Netty 到底是什么关系?一张图讲清 Java 异步技术栈选型
java·后端
m沐沐3 小时前
【计算机视觉】OpenCV 模板匹配银行卡数字识别---上
人工智能·后端·python·opencv·计算机视觉·pycharm·numpy