前言
上个月有位朋友去面试,面试官先问了什么是打破双亲委派机制,他只能照着背八股理论。紧接着面试官追问:打破这个机制到底有什么实际作用?
查阅资料后总结:双亲委派是 Java 默认的类加载规则,而打破该机制主要分为两类场景:
- 业务开发场景 :通过自定义类加载器绕开默认委派逻辑,可实现插件化、运行时热更新类逻辑、线上热修复等能力;需注意:仅打破委派无法替换已加载类,必须新建类加载器重新加载字节码才能完成更新。
- JDK / 中间件原生场景 :JDK 的 SPI 机制(如 JDBC)、Tomcat 等 Web 容器,也主动打破了双亲委派,目的是解决类加载层级限制、实现多应用类隔离与 Jar 包版本兼容。
顺着这个思路,下面我们就一步步深入,设计打破双亲委派的场景。
一、类加载器实现类

二、打破双亲委派
打破双亲委派 ≠ 完全不用双亲委派机制
1、实现流程逻辑
- 重写 loadClass() 方法
- 对于 自定义的com.nl.xx.* 包下的类不调用 super.loadClass()
- 不委托给父类,直接自己处理,直接调用自己的 findClass() 方法
- 实现自定义的类查找和加载逻辑
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 字节码增强
- 你的场景:透明替换类的实现