ByteBuddy加载动态类时抛出已加载类ClassNotFoundException问题
问题结论
ByteBuddy 原生支持动态生成类持有其他动态生成类实例引用的场景,你当前抛出ClassNotFoundException完全是代码实现逻辑错误导致的,和框架能力无关。
根因分析
你代码里有两个致命逻辑错误:
- 类结构定义顺序错误:你是先配置构造函数的字段赋值拦截逻辑,之后才调用
defineField给类添加私有字段。ByteBuddy的链式Builder是按调用顺序生成字节码结构的,你配置构造函数拦截时,目标字段还没被注册到类的结构元数据里,生成的字节码里字段引用的类型解析会出现异常。 - 类加载器上下文隔离问题:你默认调用的
unloadedType.load(SYSTEM_CLASS_LOADER)方法,用的是ByteBuddy默认的WRAPPER类加载策略,这个策略不会把字节码直接注入到你传入的系统类加载器,而是每次调用都会新建一个独立的、以系统类加载器为父级的子包装类加载器来加载新生成的类。也就是说你先加载B类时用了一个独立的子加载器,后续加载A类时又新建了另一个完全独立的子加载器,新加载器看不到之前另一个子加载器里加载的B类,自然会抛出B类找不到的异常。 - 额外提示:你当前在构造A类的拦截逻辑时,提前实例化了全局唯一的B实例,直接把这个实例硬编码到字节码中赋值给A的字段,会导致所有A类实例共享同一个B对象,如果你的业务预期是每个A实例持有独立的B对象,这个写法也需要调整。
修正方案
按以下步骤调整代码即可:
- 调整Builder的调用顺序:先定义所有字段,再配置构造函数的拦截逻辑,保证配置字段赋值时,字段已经完成声明。
- 统一类加载上下文:要么把所有动态生成的Child类和Parent类的字节码收集到一起,一次性用同一个类加载策略加载;要么改用支持往目标类加载器注入字节码的加载策略(Java 8环境可直接用
INJECTION策略,高版本Java用CHILD_FIRST策略并复用同一个加载器实例加载所有动态类)。
修正后的核心代码参考:
private static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader(); // 复用同一个类加载策略,避免多加载器隔离 private static final ClassLoadingStrategy<ClassLoader> LOAD_STRATEGY = ClassLoadingStrategy.Default.INJECTION; public Class<? extends Parent> generateParentClass(ParentConfig config) throws Exception { Map<String, Class<? extends Child>> fieldMap = config.getChildren().stream() .collect(Collectors.toMap(ChildConfig::getName, this::generateChildClass)); // 第一步:先定义所有字段,不要先写构造函数逻辑 DynamicType.Builder<Parent> typeBuilder = new ByteBuddy() .subclass(Parent.class) .name(Parent.class.getPackageName() + "." + config.getType()) .annotateType(Parent.class.getAnnotations()); for(Map.Entry<String, Class<? extends Child>> entry : fieldMap.entrySet()) { typeBuilder = typeBuilder.defineField(entry.getKey(), entry.getValue(), Modifier.PRIVATE); } // 第二步:字段定义完成后,再配置构造函数拦截逻辑 Implementation.Composable constructorInterceptor = MethodCall.invoke(Parent.class.getConstructor()); for(Map.Entry<String, Class<? extends Child>> entry : fieldMap.entrySet()) { Child child = entry.getValue().getDeclaredConstructor().newInstance(); constructorInterceptor = constructorInterceptor.andThen(FieldAccessor.ofField(entry.getKey()).setsReference(child)); } typeBuilder = typeBuilder.constructor(any()).intercept(constructorInterceptor); DynamicType.Unloaded<Parent> unloadedType = typeBuilder.make(); // 用统一的加载策略加载,保证能找到同上下文加载的Child类 return unloadedType.load(SYSTEM_CLASS_LOADER, LOAD_STRATEGY).getLoaded(); } private Class<? extends Child> generateChildClass(ChildConfig config) { DynamicType.Builder<Child> typeBuilder = new ByteBuddy() .subclass(Child.class) .name(Child.class.getPackageName() + "." + config.getName()) .annotateType(Child.class.getAnnotations()); for(Attribute attribute : config.getAttributes()) { typeBuilder = typeBuilder.defineField(attribute.getName(), attribute.getTypeClass(), Modifier.PRIVATE); } DynamicType.Unloaded<Child> unloadedType = typeBuilder.make(); // 用同一个加载策略加载到系统类加载器,保证后续Parent类能找到该类型 return unloadedType.load(SYSTEM_CLASS_LOADER, LOAD_STRATEGY).getLoaded(); }
如果你使用Java 9及以上版本,
INJECTION策略受模块访问限制无法使用,可以替换为ClassLoadingStrategy.Default.CHILD_FIRST,注意把所有动态类都通过同一个包装类加载器加载即可,不要每次load都新建加载器。
内容的提问来源于stack exchange,提问作者Deepak Kapoor
相关产品推荐
相关产品推荐

