基于 JavaAgent 代理技术实现 class 字节码插桩(bytebuddy)
先描述一个场景,生产有一个正在运行的java项目,以某 springboot-service.jar 为例,项目发布后发现了某个http接口响应较慢,此时你希望定位这个http接口执行过程中依次调用的几个主要方法的分别执行耗时,用来作为进一步解决问题的依据。你应该怎么做?Java Agent 技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强。
先描述一个场景,生产有一个正在运行的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)
更多推荐
所有评论(0)