先描述一个场景,生产有一个正在运行的java项目,以某 springboot-service.jar 为例,项目发布后发现了某个http接口响应较慢,此时你希望定位这个http接口执行过程中依次调用的几个主要方法的分别执行耗时,用来作为进一步解决问题的依据。你应该怎么做?

Java Agent 技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强。

所以本文基于 javaagent,在 JVM 加载 class 的时候,使用 bytebuddy 库动态修改 class 的方式来实现上述场景,比如我们给主要方法增加统计方法执行时间的逻辑

当然这个 bytebuddy 和 spring的 AOP 切面是有本质区别的,AOP 切面是基于 java 实例运行时进行的动态代理,bytebuddy 是直接在 class 被加载到 JVM 之前修改了 class 字节码的原理。所以使用 bytebuddy 处理过的类,对于程序中本来就存在互相依赖调用的逻辑,在执行时也是包含修改后的行为的。

应用实例

创建一个普通的 Gradle Java 工程、编写用于模拟日常项目业务的 Java 代码、添加主要依赖 shadow 和 bytebuddy 和对应配置、编写 bytebuddy 方法代理业务处理类 和 agent 入口类并使用 bytebuddy 代理 Instrumentation,最后整体实例代码工程截图和批注介绍如下:

在这里插入图片描述

代码如下,可以逐个拷贝整理到工程中执行运行查看效果:

1、build.gradle

plugins {
    id "java"
    // Shadow是一个Gradle插件,用于将项目的依赖类和资源组合到单个输出Jar中
    // https://imperceptiblethoughts.com/shadow/introduction/#benefits-of-shadow
    id "com.github.johnrengelman.shadow" version "8.1.1"
}

group = "com.shanhy.example"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

// Set the JVM compatibility versions
tasks.withType(JavaCompile).configureEach {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
    compileJava.options.encoding = "UTF-8"
    compileTestJava.options.encoding = "UTF-8"
}

dependencies {
    // 运行时依赖
    implementation 'net.bytebuddy:byte-buddy:1.12.6'
    implementation 'net.bytebuddy:byte-buddy-agent:1.12.6'
}

shadowJar {
    manifest {
        attributes(
            'Premain-Class': 'com.shanhy.example.agent.PreAgent'
        )
    }
}

2、Hello.java

package com.shanhy.example.agent;

public class Hello {

    private static final String NAME = "Tom";

    public static String hello(String name) {
        System.out.println("world");
        try {
            // 模拟暂停一秒
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "Hello ".concat(name);
    }

    public void show() {
        System.out.println("Hello Show, " + NAME);
    }

    public String show2() {
        System.out.println("Hello Show2, " + NAME);
        return "秀er";
    }

}

3、ProbeTest.java

package com.shanhy.example.agent;

/**
 * 使用方法,运行时添加VM配置
 * VM options:
 * -javaagent:工程的路径\build\libs\probe-agent-1.0-SNAPSHOT-all.jar
 *
 * @author 单红宇
 * @date 2024-03-07 19:12:18
 */
public class ProbeTest {

    /**
     * 测试时,不要直接运行该main方法,请把 Hello 和 ProbeTest 这两个类放到一个独立的java工程中运行测试
     * 本项目方到test中仅仅是为了参考示例
     *
     * @param args args
     */
    public static void main(String[] args) {
        System.out.println("Probe test");
        new Hello().show();
        System.out.println(new Hello().show2());

        System.out.println(Hello.hello("Jack"));
    }

}

4、PreAgent.java

package com.shanhy.example.agent;


import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.Instrumentation;

/**
 * 代理入口类
 *
 * @author 单红宇
 * @date 2024-03-06 17:22:31
 */
public class PreAgent {

    /**
     * premain
     *
     * @param agentArgs 代理参数
     * @param inst      Instrumentation 是Java提供的一个接口,提供了类定义和类加载相关的服务,
     *                  允许在类加载期间动态地修改类文件字节码,也就是所谓的字节码增强或字节码操作
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        // listener 用来监听扫描和处理的class情况,比如哪些处理了,哪些发生异常了
        // 开发调试阶段 onError 方法很重要,可以方法签名和参数不匹配等错误问题
        AgentBuilder.Listener listener = new AgentBuilder.Listener() {
            @Override
            public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
                // System.out.println("onDiscovery: " + typeName);
            }

            @Override
            public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) {
                // System.out.println("onTransformation: " + typeDescription.getName());
            }

            @Override
            public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) {
                // System.out.println("onIgnored: " + typeDescription.getName());
            }

            @Override
            public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {
                System.out.println("onError: " + typeName);
                throwable.printStackTrace();
            }

            @Override
            public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
                // System.out.println("onComplete: " + typeName);
            }

        };

        // transform 指定的转换器来对匹配到的class进行操作
        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
            return builder
//                    .method(ElementMatchers.named("executeInternal")) // 拦截指定的方法executeInternal
//                    .method(ElementMatchers.any()) // 拦截任意方法
                    .method(ElementMatchers.not(ElementMatchers.isStatic())) // 拦截非静态方法
                    .intercept(MethodDelegation.to(com.shanhy.example.agent.MonitorMethod.class)); // 将拦截到的方法委托给目标类处理
        };

        // type 通过ElementMatcher 来匹配我们加载的class
        new AgentBuilder
                .Default()
//                .type(ElementMatchers.nameStartsWith("com.mysql.cj.jdbc.ClientPreparedStatement"))
                .type(ElementMatchers.nameStartsWith("com.shanhy.example.agent"))
//                .type(ElementMatchers.any())
                .transform(transformer)
//                .with(listener)
                .installOn(inst);

        // 第二种匹配和增强规则
        new AgentBuilder.Default()
                .type(ElementMatchers.nameStartsWith("com.shanhy.example.agent.Hello"))
                .transform((builder, typeDescription, classLoader, module) ->
                        // 所有静态方法并且排除掉静态的构造方法、排除掉void方法、排除到static构造方法
                        builder.visit(Advice.to(com.shanhy.example.agent.MonitorStaticMethod.class).on(ElementMatchers.isStatic()
                                .and(ElementMatchers.not(ElementMatchers.returns(TypeDescription.VOID)))
                                .and(ElementMatchers.not(ElementMatchers.isTypeInitializer()))))
                )
                .with(listener)
                .installOn(inst);

    }

}

5、MonitorMethod.java

package com.shanhy.example.agent;

import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

/**
 * 监听拦截方法
 *
 * @author 单红宇
 * @date 2024-03-07 15:25:50
 */
public class MonitorMethod {

    /**
     * 方法拦截处理器。
     * 方法使用注解 @RuntimeType 告诉 Byte Buddy 不要进行严格的参数类型检测,
     * 在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法
     *
     * @param obj      使用 @This 注入被拦截的目标对象
     * @param method   使用 @Origin 注入目标方法对应的 Method 对象
     * @param callable 我们要在方法中调用目标方法的话,需要通过 @SuperCall 注入
     * @param args     使用 @AllArguments 注入目标方法的全部参数
     * @return 方法执行结果
     * @throws Exception Exception
     */
    @RuntimeType
    public static Object intercept(@This Object obj, @Origin Method method, @SuperCall Callable<?> callable,
                                   @AllArguments Object[] args) throws Exception {
        long start = System.currentTimeMillis();
        Object resObj = null;
        try {
            resObj = callable.call();
            return resObj;
        } finally {
            System.out.println("所属类名:" + method.getDeclaringClass());
            System.out.println("方法名称:" + method.getName());
            System.out.println("入参个数:" + method.getParameterCount());
            for (int i = 0; i < method.getParameterCount(); i++) {
                System.out.println("入参-" + (i + 1) + ":类型:" + method.getParameterTypes()[i].getTypeName() + " 内容:" + args[i]);
            }
            System.out.println("出参类型:" + method.getReturnType().getName());
            System.out.println("出参结果:" + resObj);
            System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
        }
    }

}

6、MonitorStaticMethod.java

package com.shanhy.example.agent;

import net.bytebuddy.asm.Advice;

import java.lang.reflect.Method;

/**
 * 监听拦截static方法。
 * ByteBuddy的MethodDelegation不能直接应用于静态方法。但可以使用ByteBuddy的Advice API来拦截静态方法。
 *
 * @author 单红宇
 * @date 2024-03-07 15:25:50
 */
public class MonitorStaticMethod {

    @Advice.OnMethodEnter
    public static void beforeStatic(@Advice.Local("startTime") long startTime, @Advice.Origin Method method, @Advice.AllArguments Object[] args) {
        // 在方法执行前的处理逻辑
        System.out.print("beforeStatic >>> " + method.getName() + ", args = ");
        System.out.println(args != null && args.length > 0 ? args[0] : null);
        startTime = System.currentTimeMillis();
    }

    /**
     * static方法执行结束
     * 注意:在使用@Advice.Return时,需要准确区分void方法或者有返回值的方法,
     * void方法不能包含@Advice.Return参数,并且对应的返回值类型要和方法返回值类型对应
     *
     * @param method    method
     * @param result    result
     * @param throwable throwable
     */
    @Advice.OnMethodExit(onThrowable = Throwable.class)
    public static void afterStatic(@Advice.Local("startTime") long startTime,
                                   @Advice.Origin Method method,
                                   @Advice.Return(readOnly = false) String result,
                                   @Advice.Thrown Throwable throwable) {
        // 在方法执行后的处理逻辑
        System.out.println("afterStatic >>> " + method.getName() + ", result = " + result);

        // 想要修改返回值,需要设定readOnly = false
        result = "[New] " + result;
        System.out.println("返回值被修改为:" + result);
        System.out.println("方法执行耗时:" + (System.currentTimeMillis() - startTime));
    }

}

最后执行 gradle 任务 shadowJar 进行打包。

运行测试

为要运行的 Java 程序添加 VM options 参数,设置 -javaagent 指向打包生成的 probe-agent-1.0-SNAPSHOT-all.jar,例如下图所示:

在这里插入图片描述

如果是命令行运行,则示例如下(注意将 javaagent 放在前面)

java -javaagent:目录\probe-agent-1.0-SNAPSHOT-all.jar -jar demo-hello.jar

下面执行日志为本例的对 Hello 中方法拦截插桩处理前和插桩处理后的分别日志:

不增加 javaagent 的日志如下:

> Task :ProbeTest.main()
Probe test
Hello Show, Tom
Hello Show2, Tom
秀er
world
Hello Jack

使用 javaagent 之后的日志如下:

Probe test
Hello Show, Tom
所属类名:class com.shanhy.example.agent.Hello
方法名称:show
入参个数:0
出参类型:void
出参结果:null
方法耗时:0ms
Hello Show2, Tom
所属类名:class com.shanhy.example.agent.Hello
方法名称:show2
入参个数:0
出参类型:java.lang.String
出参结果:秀er
方法耗时:0ms
秀er
beforeStatic >>> hello, args = Jack
world
afterStatic >>> hello, result = Hello Jack
返回值被修改为:[New] Hello Jack
方法执行耗时:1000
[New] Hello Jack

源代码地址:https://gitee.com/catoop/probe-agent


(END)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建,欢迎商务合作。wx: diudiu5555

更多推荐