byte buddy 实现链路上所有方法耗时打印
# 1. 需求背景
业务上需要实现对指定包路径下,所有方法的请求耗时进行打印,要求尽量不要与核心业务耦合。
# 2. 实现思路
如果想实现对业务尽量无感,且打印每个方法的耗时,先排除掉:在业务中直接进行日志打印耗时。主要实现思路有:
- 利用 AOP 切面来实现
- 利用字节码 agent 来实现
由于网络上对于使用 AOP 切面来实现该功能的例子比较多,这里重点介绍下如何利用 字节码 agent 来实现,具体的字节码相关的,用的是 bytebuddy 来实现。
# 3. 基于 bytebuddy 实现
基于 bytebuddy 实现的步骤:
# 3.1 maven 项目java-agent-demo
- 项目的
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">
<groupId>com.tim</groupId>
<version>1.0.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<artifactId>java-agent-demo</artifactId>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!-- 自动添加 META-INF/MAINFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>PreMainAgent</Premain-Class>
<Agent-Class>PreMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
- 新增一个类
PreMainAgent.java
,里面添加静态方法premain
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
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;
public class PreMainAgent {
// 定义要拦截的包路径前缀
private static final String PACKAGE_NAME_PREFIX = "com.agent";
public static void premain(String agentparam, Instrumentation inst) {
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer(){
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
// method 指定哪些方法需要被拦截,ElementMatchers.any 指定了所有的方法
// 声明 intercept 拦截器
return builder.method(ElementMatchers.<MethodDescription>any())
.intercept(MethodDelegation.to(MyInterceptor.class));
}
};
// type 指定了 agent 拦截的包名,以com.agent 作为前缀
// 指定了 transformer
// 将配置安装到 Instrumentation
new AgentBuilder.Default().type(ElementMatchers.<TypeDescription>nameStartsWith("com.agent")).transform(transformer).installOn(inst);
}
}
- 新增一个拦截类
MyInterceptor.java
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class MyInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable) throws Exception {
Long start = System.currentTimeMillis();
try {
return callable.call();
} finally {
System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
}
}
}
执行
mvn clean
、mvn compile
、mvn deploy
或者通过 idea中的maven ui界面进行操作在 target 下生成一个
java-agent-demo-1.0.0-SNAPSHOT.jar
# 3.2 项目 java-agent-user 来进行验证
- 新建一个普通的 java 项目
java-agent-user
- 在包路径 com.agent 下新建一个Main类下,代码如下:
package com.agent;
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("main 方法执行");
Thread.sleep(1000L);
hello();
Thread.sleep(10L);
hello2();
}
public static void hello() throws InterruptedException {
Thread.sleep(50L);
System.out.println("23");
}
public static void hello2() throws InterruptedException {
Thread.sleep(10L);
System.out.println("33");
}
}
# 3.3 在工程 java-agent-user 中执行命令
java -javaagent:/${路径}/java-agent-demo-1.0.0-SNAPSHOT.jar -jar java-agent-user.jar
或者直接在idea的VM options中执行如下命令:
-javaagent:/%{路径}/java-agent-demo-1.0.0-SNAPSHOT.jar
说明:${路径} 请换成实际打成 agent 的包路径。
# 3.4 执行效果
main 方法执行
23
hello:52ms
33
hello2:12ms
main:1078ms
# 4. 总结
利用 bytebuddy 可以轻松的实现字节码的插砖技术,实现业务解耦打印每个方法的耗时。
上次更新: 2023/01/31, 09:21:58