重写 loadclass 会破坏双亲委派,因其绕过父加载器优先加载逻辑;正确做法是仅重写 findclass 并调用 defineclass,避免直接干预委派流程。

为什么重写 loadClass 会破坏双亲委派,但多数人其实不该动它
Java 类加载器默认走双亲委派:先让父加载器尝试加载,失败才自己加载。真正打破它的关键不是“自定义类加载器”,而是**绕过 loadClass 的默认逻辑**——比如直接重写该方法却不调用 super.loadClass(name),或在其中插入自己的加载路径优先级。
但绝大多数场景下,你根本不需要碰 loadClass。Tomcat、OSGi、热部署框架等确实打破了委派,但它们是为了解决「隔离性」和「版本冲突」这类强约束问题,不是为了“炫技”。盲目重写只会导致 NoClassDefFoundError 或 LinkageError,尤其在反射、序列化、JDBC 驱动加载时悄无声息地崩。
- 正确做法:继承
ClassLoader,只重写findClass(String name),并在里面调用defineClass() - 错误高发点:在
loadClass里直接 new byte[] +defineClass,却没处理resolve参数,导致类未链接,后续调用时报IllegalAccessError - 兼容性风险:JDK 9+ 引入模块系统后,
defineClass对非java.*包的类会校验Module和ProtectionDomain,硬塞字节码可能被拒绝
Tomcat 的 WebAppClassLoader 是怎么避开双亲委派的
它没全盘否定双亲委派,而是做了「有选择的委派」:对 /WEB-INF/classes 和 /WEB-INF/lib/*.jar 中的类,**优先自己加载**;对 javax.*、org.apache.catalina.* 等核心包,则强制委派给父加载器(避免容器与应用类混用同一份实现)。
这个逻辑藏在 WebAppClassLoader.loadClass() 的 if-else 分支里,不是靠删代码,而是靠判断类名前缀 + 资源路径是否存在。
立即学习“Java免费学习笔记(深入)”;
- 关键判断:是否匹配
delegate配置(默认 false),以及类名是否属于delegateLoad白名单(如java.、javax.) - 资源定位陷阱:用
getResourceAsStream("META-INF/MANIFEST.MF")可能拿到父加载器的 jar,但用findResource()才走当前类加载器路径 - 线程上下文加载器(
Thread.currentThread().getContextClassLoader())在这里被设为WebAppClassLoader,所以ServiceLoader、JDBCDriverManager才能感知到应用自己的驱动
defineClass 的字节码来源必须可信,否则 JVM 直接拒绝
即使你绕过了委派,把字节码喂给 defineClass,JVM 仍会校验:name 是否与字节码中声明的类名一致、是否已由同一类加载器定义过、是否违反模块封装(JDK 9+)、是否含非法字节码结构。校验失败抛 ClassFormatError 或 SecurityException。
常见翻车点是动态生成类(ASM/Javassist)后没清理调试信息,或从网络加载字节码却没校验签名——JDK 默认启用 SecurityManager 时,defineClass 会检查 RuntimePermission("defineClass")。
- 安全建议:生产环境禁用
SecurityManager后,仍需校验字节码来源(如比对 SHA256),防止恶意注入 - 调试技巧:用
javap -v检查生成类的ConstantPool和Signature属性是否合法 - 性能注意:反复调用
defineClass会触发元空间(Metaspace)增长,若类不卸载,容易 OOM;Tomcat 通过clearReferencesThreads()清理线程引用缓解此问题
自定义类加载器和 Spring 的 ClassLoader 交互很脆弱
Spring 容器启动时,默认使用当前线程上下文加载器(TCCL)加载配置类、Bean 定义、注解处理器。如果你手动 new 出一个类加载器并传给 new ClassPathXmlApplicationContext(...),Spring 内部仍可能在某些路径(如 AOP 代理生成、EL 表达式解析)回退到 Class.forName(...),从而走系统类加载器,导致类型不匹配:同一个类名,被两个加载器加载,instanceof 返回 false,cast 报 ClassCastException。
- 典型症状:
org.springframework.beans.factory.BeanNotOfRequiredTypeException,提示 “expected type X, but got Y”,实则是 X 和 Y 是不同加载器加载的同名类 - 解决思路:统一使用
Thread.currentThread().setContextClassLoader(yourLoader),并在 Spring 上下文刷新前设置好;避免在 Bean 初始化中临时切换 TCCL - 更隐蔽的坑:Spring Boot 的
LaunchedURLClassLoader本身已打破委派(优先加载BOOT-INF/classes),再套一层自定义加载器极易引发嵌套委派混乱
真正难的不是写个能加载类的加载器,而是让整个生态(JVM、容器、框架、第三方库)都按你的委派规则走——稍有不一致,就变成运行时类型系统崩塌,而这种问题往往只在特定环境复现,日志里连堆栈都藏得极深。










