在ByteBuddy中用Jacoco instrument替换字节码并实现加载的技术问询
问题整理
1. ByteBuddy Listener中替换Jacoco处理后的字节码如何让ByteBuddy加载?
我希望在ByteBuddy的AgentBuilder.Listener的onTransformation()方法中,用Jacoco的instrument()生成新的byte[],让ByteBuddy加载这个替换后的字节码,当前代码如下:
@Override public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType){ System.out.println("onTransformation=" + typeDescription.getName() + ",classLoader=" + classLoader); byte[] bytes = dynamicType.getBytes(); try { byte[] instrument = new Instrumenter(new LoggerRuntime()).instrument(bytes, typeDescription.getName()); } catch (IOException e) { throw new RuntimeException(e); } dynamicType.inject(); }
2. Attach模式下JavaAgent如何完成任意类的byte[]替换?
我尝试过两种方案,但都有问题:
方案一:AgentBuilder.Transformer修改,出现类重复加载问题
代码如下:
public class AgentTransform implements AgentBuilder.Transformer { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, ProtectionDomain protectionDomain) { try { System.out.println("AgentTransform="+typeDescription.getName()); Class<?> aClass = classLoader.loadClass(typeDescription.getName()); byte[] classBytes = getClassBytes(aClass); byte[] instrument = AgentConfig.Coverage.INSTRUMENTER.instrument(classBytes, typeDescription.getName()); builder.require(typeDescription, instrument); AgentConfig.Coverage.COVERAGE_MAP.put(typeDescription.getName(),classBytes); return builder; } catch (Exception e) { throw new RuntimeException(e); } } public byte[] getClassBytes(Class<?> clazz) throws IOException { String className = clazz.getName().replace('.', '/') + ".class"; InputStream inputStream = clazz.getClassLoader().getResourceAsStream(className); if (inputStream == null) { throw new IOException("Class not found: " + className); } byte[] buffer = new byte[8192]; int bytesRead; ByteArrayOutputStream output = new ByteArrayOutputStream(); while ((bytesRead = inputStream.read(buffer)) != -1) { output.write(buffer, 0, bytesRead); } return output.toByteArray(); } }
方案二:遍历已加载类调用redefineClasses,出现类型转换异常
代码如下:
Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class loadedClass : allLoadedClasses) { try { System.out.println("loadedClass=" + loadedClass); // inst.retransformClasses(loadedClass); byte[] classBytes = getClassBytes(loadedClass); byte[] instrument = AgentConfig.Coverage.INSTRUMENTER.instrument(classBytes, loadedClass.getName()); inst.redefineClasses(new ClassDefinition(loadedClass,instrument)); AgentConfig.Coverage.COVERAGE_MAP.put(loadedClass.getName(),classBytes); } catch (Exception e) { throw new RuntimeException(e); } }
报错堆栈:
java.lang.BootstrapMethodError: bootstrap方法初始化异常 at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:194) ~[na:na] at java.base/java.lang.invoke.ConstantBootstraps.makeConstant(ConstantBootstraps.java:67) ~[na:na] at java.base/java.lang.invoke.MethodHandleNatives.linkDynamicConstantImpl(MethodHandleNatives.java:314) ~[na:na] at java.base/java.lang.invoke.MethodHandleNatives.linkDynamicConstant(MethodHandleNatives.java:306) ~[na:na] at com.bci.test.benchmark.exceptions.GlobalExceptionHandler.handlerRuntimeException(GlobalExceptionHandler.java:49) ~[classes/:na] ... Caused by: java.lang.ClassCastException: class java.lang.Long cannot be cast to class [Z (java.lang.Long和[Z位于引导类加载器的java.base模块中) at com.bci.test.benchmark.exceptions.GlobalExceptionHandler.$jacocoInit(Unknown Source) ~[classes/:na] ... 64 common frames omitted
java.lang.ClassCastException: class java.lang.Long cannot be cast to class [Z (java.lang.Long和[Z位于引导类加载器的java.base模块中) at com.bci.test.benchmark.api.protocol.ProtocolHttpApi.$jacocoInit(Unknown Source) ~[classes/:na] ...
解决方案
一、Listener中替换字节码的正确方式
AgentBuilder.Listener的onTransformation是事件回调,并非修改类定义的入口。如果要替换成Jacoco处理后的字节码,不能直接调用dynamicType.inject()(这会加载原始的ByteBuddy生成类),正确做法是借助Instrumentation直接定义类:
// 确保你持有Instrumentation实例 private final Instrumentation inst; @Override public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType){ System.out.println("onTransformation=" + typeDescription.getName() + ",classLoader=" + classLoader); byte[] bytes = dynamicType.getBytes(); try { byte[] instrumentedBytes = new Instrumenter(new LoggerRuntime()).instrument(bytes, typeDescription.getName()); // 类未加载时直接定义 if (!loaded) { inst.defineClass(typeDescription.getName(), instrumentedBytes, 0, instrumentedBytes.length, classLoader, dynamicType.getProtectionDomain()); } else { // 类已加载时执行重定义 inst.redefineClasses(new ClassDefinition(typeDescription.resolve(), instrumentedBytes)); } } catch (IOException | UnmodifiableClassException e) { throw new RuntimeException(e); } }
注意:处理已加载类的重定义时,需确保类支持重定义,且ByteBuddy的AgentBuilder已启用重定义策略。
二、Attach模式下类替换的正确实现
Attach模式下需区分未加载类和已加载类分别处理:
1. 处理未加载类:用ByteBuddy Transformer注入Jacoco字节码
不要在Transformer中加载原类(会触发提前加载导致重复加载问题),直接从类路径读取原始字节码进行Instrument,再通过ByteBuddy重构类定义:
public class AgentTransform implements AgentBuilder.Transformer { @Override public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, ProtectionDomain protectionDomain) { try { // 从类路径读取原始字节码,不加载类 String classNamePath = typeDescription.getName().replace('.', '/') + ".class"; InputStream inputStream = classLoader.getResourceAsStream(classNamePath); if (inputStream == null) { return builder; } // 读取字节码(可自行实现或用Apache Commons IO的IOUtils) byte[] buffer = new byte[8192]; int bytesRead; ByteArrayOutputStream output = new ByteArrayOutputStream(); while ((bytesRead = inputStream.read(buffer)) != -1) { output.write(buffer, 0, bytesRead); } byte[] originalBytes = output.toByteArray(); byte[] instrumentedBytes = AgentConfig.Coverage.INSTRUMENTER.instrument(originalBytes, typeDescription.getName()); // 用Instrument后的字节码重构builder return new ByteBuddy().redefine(instrumentedBytes, typeDescription) .make() .builder(); } catch (Exception e) { throw new RuntimeException(e); } } }
同时配置AgentBuilder启用重定义策略:
new AgentBuilder.Default() .type(ElementMatchers.any()) // 按需调整类匹配规则 .transform(new AgentTransform()) .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .installOn(inst);
2. 处理已加载类:解决Jacoco重复Instrument问题
你遇到的ClassCastException是因为类被重复Instrument:Jacoco会给类注入$jacocoInit方法和静态变量,重复处理会导致常量池类型冲突。解决方法是先判断类是否已被处理:
Class[] allLoadedClasses = inst.getAllLoadedClasses(); for (Class loadedClass : allLoadedClasses) { try { // 跳过已被Jacoco处理的类(检查是否存在$jacocoData静态字段) loadedClass.getDeclaredField("$jacocoData"); continue; } catch (NoSuchFieldException e) { // 未被处理,执行Instrument和重定义 try { byte[] classBytes = getClassBytes(loadedClass); byte[] instrumentedBytes = AgentConfig.Coverage.INSTRUMENTER.instrument(classBytes, loadedClass.getName()); // 先启用重转换再执行重定义 inst.retransformClasses(loadedClass); inst.redefineClasses(new ClassDefinition(loadedClass, instrumentedBytes)); AgentConfig.Coverage.COVERAGE_MAP.put(loadedClass.getName(),classBytes); } catch (Exception ex) { throw new RuntimeException(ex); } } catch (Exception e) { throw new RuntimeException(e); } }
注意:
- 系统核心类、已初始化的final类等不支持重定义,需跳过
- Attach模式下需确保Jacoco Runtime已正确初始化,否则
$jacocoInit会执行失败
三、关键注意事项
- 避免重复Instrument:无论处理未加载还是已加载类,都要先判断是否已被Jacoco处理
- 不在Transformer中加载原类:会触发类提前加载,导致ByteBuddy无法拦截修改
- 正确使用Instrumentation API:未加载类用
defineClass,已加载类用redefineClasses/retransformClasses,且需类支持重定义
内容的提问来源于stack exchange,提问作者user9178223
相关产品推荐
相关产品推荐

