先描述一个场景,生产有一个正在运行的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-shadowid "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_8targetCompatibility = JavaVersion.VERSION_1_8compileJava.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");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 {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() {@Overridepublic void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {// System.out.println("onDiscovery: " + typeName);}@Overridepublic void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) {// System.out.println("onTransformation: " + typeDescription.getName());}@Overridepublic void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) {// System.out.println("onIgnored: " + typeDescription.getName());}@Overridepublic void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {System.out.println("onError: " + typeName);throwable.printStackTrace();}@Overridepublic 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 来匹配我们加载的classnew 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*/@RuntimeTypepublic 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.OnMethodEnterpublic static void beforeStatic(@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);}/*** 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.Origin Method method,@Advice.Return(readOnly = false) String result,@Advice.Thrown Throwable throwable) {// 在方法执行后的处理逻辑System.out.println("afterStatic >>> " + method.getName() + ", result = " + result);// 想要修改返回值,需要设定readOnly = falseresult = "[New] " + result;System.out.println("返回值被修改为:" + result);}}
最后执行 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 中方法拦截插桩处理后的日志:
> Task :ProbeTest.main()
Probe test
Hello Show, Tom
所属类名:class com.shanhy.example.agent.Hello
方法名称:show
入参个数:0
出参类型:void
出参结果:null
方法耗时:1ms
所属类名:class com.shanhy.example.agent.Hello
方法名称:show
入参个数:0
出参类型:void
出参结果:null
方法耗时:2ms
Hello Show2, Tom
所属类名:class com.shanhy.example.agent.Hello
方法名称:show2
入参个数:0
出参类型:java.lang.String
出参结果:秀er
方法耗时:0ms
所属类名:class com.shanhy.example.agent.Hello
方法名称:show2
入参个数:0
出参类型:java.lang.String
出参结果:秀er
方法耗时:0ms
秀er
beforeStatic >>> hello, args = Jack
beforeStatic >>> hello, args = Jack
world
afterStatic >>> hello, result = Hello Jack
返回值被修改为:[New] Hello Jack
afterStatic >>> hello, result = [New] Hello Jack
返回值被修改为:[New] [New] Hello Jack
[New] [New] Hello Jack
(END)