ByteBuddy本地正常Docker报错:ClassLoader差异及方案咨询
ByteBuddy本地运行正常,Docker部署后抛出ClassNotFoundException的原因分析
问题背景
ByteBuddy实现在IntelliJ本地运行正常,但打包为Jar部署到Docker容器后抛出ClassNotFoundException。
初始实现(本地正常,Docker报错)
private DynamicType.Loaded<? extends MyClassToRedefine> getLoadedTypeOfResource() { if (myClassToRedefineImplLoadedType == null) { try (var unloadedResource = new ByteBuddy() .subclass(MyClassToRedefine.class) .annotateType(newAnnotation1) .annotateType(newAnnotation2) .annotateType(newAnnotation3) .make()) { myClassToRedefineImplLoadedType = unloadedResource.load(ClassLoader.getSystemClassLoader()); } } return myClassToRedefineImplLoadedType; }
Docker运行环境
使用以下Dockerfile打包运行:
FROM openjdk:21-jdk ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","/app.jar"]
抛出的异常
Caused by: java.lang.ClassNotFoundException: com.abc.def.MyClassToRedefine at net.bytebuddy.dynamic.loading.ByteArrayClassLoader.findClass(ByteArrayClassLoader.java:404) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) ... 73 common frames omitted
修复后的实现(本地和Docker均正常)
private DynamicType.Loaded<? extends MyClassToRedefine> getLoadedTypeOfResource() { ByteBuddyAgent.install(); if (provisionedCustomResourceImplLoadedType == null) { try (var unloadedResource = new ByteBuddy() .subclass(MyClassToRedefine.class) .annotateType(newAnnotation1) .annotateType(newAnnotation2) .annotateType(newAnnotation3) .make()) { myClassToRedefineImplLoadedType = unloadedResource.load(MyClassToRedefine.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); } } return myClassToRedefineImplLoadedType; }
疑问解答
1. 为何第一种方案本地正常Docker报错,第二种方案通用?
第一种方案的问题:
使用ClassLoader.getSystemClassLoader()加载动态子类时,系统类加载器(JDK的AppClassLoader)在Docker环境下无法访问MyClassToRedefine类。Spring Boot Jar运行时,应用类由LaunchedURLClassLoader(Spring Boot自定义类加载器)加载,而AppClassLoader只能加载JDK核心类和classpath根目录下的类,无法穿透Spring Boot Jar访问内部嵌套的类。- 本地IntelliJ运行时,类直接放在文件系统的classpath中,
AppClassLoader能直接找到目标类,因此加载子类正常。 - Docker环境下,应用类都在Jar内部,系统类加载器找不到父类,导致动态子类加载失败,抛出
ClassNotFoundException。
- 本地IntelliJ运行时,类直接放在文件系统的classpath中,
第二种方案的优势:
- 用
MyClassToRedefine.class.getClassLoader()获取到加载父类的Spring Boot自定义类加载器,动态子类和父类使用同一类加载器,类加载委托机制能正常找到父类。 - 引入
ByteBuddyAgent和ClassReloadingStrategy,通过Java Agent机制实现类加载/重定义,绕过普通类加载器的限制,保证了本地和Docker环境下类加载的一致性。
- 用
2. 两种类加载器在本地和Docker环境中的差异
| 类加载器调用方式 | 本地IntelliJ环境 | Docker Spring Boot Jar环境 |
|---|---|---|
ClassLoader.getSystemClassLoader() | 为AppClassLoader,可直接访问classpath下所有应用类 | 为AppClassLoader,仅能加载JDK核心类,无法访问Spring Boot Jar内部应用类 |
MyClassToRedefine.class.getClassLoader() | 为AppClassLoader(本地类直接在classpath) | 为LaunchedURLClassLoader(Spring Boot自定义类加载器,负责加载Jar内部应用类) |
简单总结:
- 本地环境下,两种类加载器本质是同一个,都能访问目标类,因此第一种方案可行。
- Docker环境下,系统类加载器和应用类加载器是不同实例,系统类加载器看不到Jar内部类,导致加载失败;使用父类的类加载器则能正确找到依赖的父类。
内容的提问来源于stack exchange,提问作者Raspberry_Sherbet
相关产品推荐
相关产品推荐

