bytebuddy与javaagent结合使用

  最近在研究可观测链路时,发现工具里在进行代码插桩时,使用的是butebuddy工具。我之前了解过asm和javaassist两种工具,对比来说asm更加高效但偏向底层,javaassist更加高级但性能略差。那需要一个折中一点的工具,让我们能够进行高效的字节码操作的同时不损失性能。bytebuddy正是这样的一个工具,他提供了较为高级的api来操作字节码,但性能依旧强悍,极大的降低了开发门槛。

  bytebuddy本身并不是一种新的字节码技术,而是对asm进行了封装,让我们不必深入了解字节码工具以及jvm指令。同时butebuddy还有一个很大的优点是其直接提供了与javaagent深入结合的工具byte-buddy-agent。我们大部分字节码操作的场景都是在使用agent上,这样可以直接使用的工具能极大的方便我们开发。

  接下来用一个较为简单的案例展示一下bytebuddy进行agent开发的步骤,案例的功能是拦截所有以com.example.demo开头的类的方法,并在方法前后注入逻辑。

 

前提

  准备好java环境以及maven(方便构建agent包以及依赖),我的版本是java8,maven3.8.1 

  需要读者对javaagent的作用以及使用有一定了解

agent包

  1.引入依赖pom.xml  

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>test-agent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <bytebuddy.version>1.14.9</bytebuddy.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>${bytebuddy.version}</version>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>${bytebuddy.version}</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>com.study.agent.StudyAgentBoot</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

  2. 编写注入代码,注意有两种方式,一种是代码嵌入式,一种是拦截器模式(名字为本人理解)。两种方式的区别后面会讲,这儿新建两种代码增强类,功能都是一样的,函数执行前后打印出函数的入参以及出参,有异常时打印异常

  2.1 代码嵌入式增强类

public class MethodTracer {

    @Advice.OnMethodEnter
    public static void onMethodEnter(
            @Advice.Origin String method,
            @Advice.AllArguments Object[] args) {
        System.out.println("Entering: " + method + "||   params" + Arrays.toString(args));
    }

    @Advice.OnMethodExit(onThrowable = Throwable.class)
    public static void onMethodExit(
            @Advice.Origin String method,
            @Advice.Return Object returnValue,
            @Advice.Thrown Throwable throwable) {
        if (throwable == null){
            System.out.println("Exiting: " + method + " with result: " + returnValue);
        }else {
            System.out.println("Exception in: " + method + ": " + throwable.getMessage());
        }
    }
}

  2.2 拦截器模式增强类

public class MethodTracer2 {

    @RuntimeType
    public Object intercept(
            @This Object obj, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper,
            @Origin Method method) throws Exception {

        System.out.println("Entering: " + method.getName() + "||   params" + Arrays.toString(allArguments));
        try {
            Object result = zuper.call();
            System.out.println("Exiting: " + method.getName() + " with result: " + result);
            return result;
        } catch (Exception e) {
            System.out.println("Exception in: " + method.getName() + ": " + e.getMessage());
            throw e;
        }
    }
}

  

  3.agent类  这儿有两个premain函数,上面的是演示代码嵌入式,下面的是拦截模式

public class StudyAgentBoot {

    public static void premain(String arguments, Instrumentation instrumentation) {
        System.out.println("Initializing MethodTraceAgent...");

        new AgentBuilder.Default()
                .type(ElementMatchers.nameStartsWith("com.example.demo"))
                .transform((builder, type, classLoader, module, protectionDomain) ->
                        builder.method(ElementMatchers.any())
                                .intercept(Advice.to(MethodTracer.class)))
                .installOn(instrumentation);
    }
//    public static void premain(String arguments, Instrumentation instrumentation) {
//        System.out.println("Initializing MethodTraceAgent...");
//        new AgentBuilder.Default()
//                .ignore(ElementMatchers.nameStartsWith("net.bytebuddy"))
//                .type(ElementMatchers.nameStartsWith("com.example.demo"))
//                .transform((builder, type, classLoader, module, protectionDomain) ->
//                        builder.method(ElementMatchers.named("findBySerialNo")).intercept(MethodDelegation.to(new MethodTracer2())))
//                .installOn(instrumentation);
//    }

}

 

  4. 命里行执行mvn clean package进行打包

测试项目

  项目比较简单  一个service类,一个main函数 模拟我们的业务方

package com.example.demo;

public class GoodsOrderService {

    public  String findBySerialNo(String serialNo){
        return "1" + serialNo + "|||";
    }
}

  

package com.example.demo;
public class Main {

    public static void main(String[] args) throws InterruptedException {
        GoodsOrderService goodsOrderService = new GoodsOrderService();
        String bySerialNo = goodsOrderService.findBySerialNo("124124");
        System.out.println(bySerialNo);
        while (true){

        }
    }
}

  在idea的启动类里面的vm.options参数里面加上agent参数  -javaagent:your workspace\test-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

 

我们在执行main函数后不出意外可以得到如下结果,可以看到我们的代码增强功能已经成功实现,入参和出参都被打印了出来

Initializing MethodTraceAgent...
Entering: public java.lang.String com.example.demo.GoodsOrderService.findBySerialNo(java.lang.String)||   params[124124]
Exiting: public java.lang.String com.example.demo.GoodsOrderService.findBySerialNo(java.lang.String) with result: 1124124|||
1124124|||

  

增强原理与两种方式

  那这个是怎么实现的呢? 这儿就涉及到上面遗留的问题,两种增强模式。想要理解这个,我们还需要一个工具,把运行时的GoodsOrderService找到并反编译看看,类具体被修改成了什么样。这儿使用java自带的工具。

  进入到本地java的bin目录下 执行如下指令 ./java -cp ../lib/sa-jdi.jar sun.jvm.hotspot.HSDB,如果顺利的话会弹出一个可交互的界面,点击左上角的file>attach to hotspot process,然后输入自己的进程id后就顺利链接上了,然后在点击左上角tools>Class Browser  搜索GoodsOrderService可以看到下面已经成功找到,

  

  嵌入模式

  可以发现类里面现在有一个findBySerialNo方法,一个findBySerialNo$original$Gca4CnId方法且加了修饰符private synthetic ,关于synthetic 修饰符,是编译器添加的修饰符,我们无法直接使用,详细的可以自己搜索了解。

image

 

  我们打开findBySerialNo$original$Gca4CnId 这个方法,由于加了synthetic修饰符,无法直接反编译,所以我们用工具看看里面的代码内容,可以发现这个方式里面的代码变成了我们原本的findBySerialNo业务代码,即在入参前拼接1,入参后拼接|||

image

   

  打开原本的findBySerialNo方法,发现原本方法已经改变了,由于这个方法内容较多,但是可以反编译,我们直接反编译看看,点上面的create.class file 然后用idea打开这个class,我们主要看看findBySerialNo方法变成啥样了

    public String findBySerialNo(String var1) {
        System.out.println("Entering: " + "public java.lang.String com.example.demo.GoodsOrderService.findBySerialNo(java.lang.String)" + "||   params" + Arrays.toString(new Object[]{var1}));
        GoodsOrderService var2 = this;
        String var3 = var1;

        String var7;
        Throwable var8;
        label24: {
            String var10000;
            try {
                var10000 = var2.findBySerialNo$original$Gca4CnId(var3);
            } catch (Throwable var6) {
                var8 = var6;
                var7 = null;
                break label24;
            }

            var7 = var10000;
            var8 = null;
        }

        if (var8 == null) {
            System.out.println("Exiting: " + "public java.lang.String com.example.demo.GoodsOrderService.findBySerialNo(java.lang.String)" + " with result: " + var7);
        } else {
            System.out.println("Exception in: " + "public java.lang.String com.example.demo.GoodsOrderService.findBySerialNo(java.lang.String)" + ": " + var8.getMessage());
        }

        if (var8 != null) {
            throw var8;
        } else {
            return var7;
        }
    }

  看到这个想必就很容易理解我说的嵌入式代码,这种增强方式的原理就是  将我们原本的业务函数修改了函数名为findBySerialNo$original$Gca4CnId,并添加修饰符synthetic,然后再生成一个新的findBySerialNo函数,将我们一开始定义的代码增强逻辑直接嵌入到函数前后,并在中间执行var2.findBySerialNo$original$Gca4CnId(var3) 也就是原本的业务逻辑,这样就做到了代码增强的功能,当然这个var2字段在反编译文件是看不到的,因为对象的属性和方法都是编译期加进去的添加了synthetic修饰符,无法反编译。不过还是可以通过上面的交互工具看到的。

  相信弄到这里如果有实践的话已经看到了该模式的缺点,就是增强代码直接嵌入到了原本函数,会导致所有函数前后都会加上这些代码显得很臃肿,那我们再来看看拦截模式

 

拦截模式

  将agent类注释逻辑调整,放开拦截模式的代码,同样的打包,执行,并使用交互工具打开类,可以看到应用直接生成了两个类GoodsOrderService,以及GoodsOrderService$auxiliary$gkF1t3FY     

image

   我们反编译后的两个类一起看

public class GoodsOrderService {
    public GoodsOrderService() {
    }

    static {
        ClassLoader.getSystemClassLoader().loadClass("net.bytebuddy.dynamic.Nexus").getMethod("initialize", Class.class, Integer.TYPE).invoke((Object)null, GoodsOrderService.class, -1440179496);
        cachedValue$ZDwrdDlh$g12muu0 = GoodsOrderService.class.getMethod("findBySerialNo", String.class);
    }

    public String findBySerialNo(String var1) {
        return (String)delegate$k378eq1.intercept(this, new Object[]{var1}, new auxiliary.gkF1t3FY(this, var1), cachedValue$ZDwrdDlh$g12muu0);
    }
}
class GoodsOrderService$auxiliary$gkF1t3FY implements Runnable, Callable {
    private GoodsOrderService argument0;
    private String argument1;

    GoodsOrderService$auxiliary$gkF1t3FY(GoodsOrderService var1, String var2) {
        this.argument0 = var1;
        this.argument1 = var2;
    }

    static {
        ClassLoader.getSystemClassLoader().loadClass("net.bytebuddy.dynamic.Nexus").getMethod("initialize", Class.class, Integer.TYPE).invoke((Object)null, GoodsOrderService$auxiliary$gkF1t3FY.class, 429360412);
    }

    public void run() {
        this.argument0.findBySerialNo$original$sgDc3lqc$accessor$ZDwrdDlh(this.argument1);
    }

    public Object call() throws Exception {
        return this.argument0.findBySerialNo$original$sgDc3lqc$accessor$ZDwrdDlh(this.argument1);
    }
}

  这儿其实可以看到  原本的GoodsOrderService的findBySerialNo已经被改造了,会直接调用我们一开始的trace2里面定义的方法intercept,那我们原本的方法调用怎么办呢,可以看到原本的业务代码以及类被重新生成封装到了GoodsOrderService$auxiliary$gkF1t3FY 。 原本的findBySerialNo也被修改成了findBySerialNo$original$sgDc3lqc$accessor$ZDwrdDlh (该方法定义在GoodsOrderService中且被synthetic修饰,内部逻辑是原本的业务代码),但是内部的业务逻辑是没有变化的。该类的入参和调用被封装成了一个callable/runnable 

  然后就回到了一开始trace2里面的逻辑

image

 

  该模式的优点是增强代码不会全部嵌入到类里面,而是所有的代码从原本逻辑改为执行该intercept逻辑,要清晰的多,且方便我们做统一管理。

 

结尾

   本文主要写明了bytebuddy在使用javaagent情况下如何进行字节码增强,可以看到使用起来是要比asm简单很多,即便不了解jvm指令和字节码规范也是可以使用的。后续介绍的两种增强模式,目前看着opentelementry用的第一种,skywalking用的第二种,看着应该是各有优劣吧。

   回到一开始的初衷,即可观测链路的实现,使用字节码工具,我们可以做到业务方无感知无代码侵入的情况下,将链路追踪逻辑写入代码,且不依赖其他的框架例如spring等。

 

posted @ 2025-09-02 19:30  雨落寒沙  阅读(14)  评论(0)    收藏  举报