You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 13:50:54