当前位置: 首页 > news >正文

颠覆认知: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、自定义类加载器

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

package com.nl.classloader.custom; public class CustomTestDefault { public void say() { System.out.println("CustomTestDefault:hello world"); } }

2.3、替换CustomTestV1

package com.nl.classloader.custom; public class CustomTestV1 { public void say() { System.out.println("CustomTestV1:hello world"); } }

2.4、执行

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、输出结果

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

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

3.1、正常流程

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

3.2、打破双亲委派

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 字节码增强
  • 你的场景:透明替换类的实现
http://www.jsqmd.com/news/1002490/

相关文章:

  • SpringBoot项目里,用QueryDSL-JPA优雅地干掉那些又臭又长的JPQL(附完整配置与实战代码)
  • PvZWidescreen宽屏补丁:3步告别黑边,让经典游戏焕发新生
  • 别再傻傻用HAL_Delay了!手把手教你用STM32F4的DWT实现微秒级精准计时
  • 从图卷积到时空预测:除了交通,STGCN模型还能用在哪些意想不到的场景?
  • 2026年新发布:厦门新闽菜餐厅深度解析,闽地私厨实力见真章 - 品牌鉴赏官2026
  • HP OMEN性能解锁工具:OmenSuperHub完整使用指南
  • 【本地 AI 自动化最新工具】 OpenClaw 2.7.9 Windows 完整部署教程(包含安装包)
  • COMSOL后处理实战:用‘表面积分’和‘过滤器’两步搞定接触面积计算(附弹簧扣案例)
  • 告别车载ECU‘失眠’:用AUTOSAR NM实现整车低功耗休眠的实战配置(附状态机详解)
  • QKeyMapper:Windows最强按键映射神器,3分钟打造你的专属操控体验
  • 2026年神仙居周边住宿选择指南:聚友居民宿与本地农家乐口碑实测分析 - 优质品牌商家
  • 长沙蔚来音响升级认准哪家权威门店?5大核心优势解锁蔚来专属音改方案,蔚来ES8音响升级,蔚来车型音响升级方案推荐 - 品牌推荐师
  • 2026年当前上海刑事会见律师专业推荐与选择全解析 - 品牌鉴赏官2026
  • 网盘直链下载助手LinkSwift:三步告别限速,九大网盘一键直链下载终极指南
  • `import openpyxl` 是 Python 中用于读写 Excel(`.xlsx`)文件的第三方库的导入语句
  • 2026年PE燃气管厂家实力之选:龙昌管业在市政埋地、天然气专用与高压大口径领域的专业解读 - 品牌发掘
  • 进阶玩家的Zotero工具箱:用Better BibTex的PostScript脚本,批量清洗和定制你的参考文献数据库
  • 从GDP到股价:手把手教你用Matlab的adftest函数检验5类真实数据的平稳性
  • 告别HDF格式!用ArcPy批量处理GLASS LAI数据,从下载到月度合成的完整避坑指南
  • 从0到1:基于Python的简单自动化任务系统设计与实现
  • Win11Debloat技术深度解析:从系统清理到企业级部署
  • 2026年浙江杭州合同纠纷律师实力对比 5家深度测评各有特色 - 本地品牌推荐
  • UEFI开发实战:手把手教你用GUID HOB在PEI和DXE间传递自定义数据
  • 【万字文档+源码】基于springboot+vue电池销售系统 -学习项目资料分享
  • 科学高效学英语:全方位提升语言综合应用能力
  • ST官方开发板uboot启动配置详解:手把手教你读懂extlinux.conf文件
  • 2026年 达因值添加剂/碳氢达因值加强剂/达因笔增大剂及专用清洗剂供应厂家:精准提升表面张力与碳氢清洗的专业选择 - 品牌发掘
  • 从Proteus仿真到FPGA管脚分配:DAC0832数模转换实战全记录(含VHDL代码参考)
  • 给Android开发者的车载入门指南:从手机App到车机SystemUI,到底有啥不一样?
  • 深耕欧洲市场,光驭科技携手Grolman首秀法国FIP 2026