新来的实习生妹妹故意刁难我,
说想让我实现一个方法耗时统计工具,
不能用切面,
这能难倒我嘛,Java Agent 安排上。
前言
本篇文章将实现一个超绝战损版的基于 Java Agent 的方法耗时统计工具。
整体内容分为:
Java Agent 原理简析;
方法耗时统计工具实现;
方法耗时工具的 Springboot 的 starter 包实现。
正文
一. Java Agent 原理简析
理解啥是 Java Agent 前,需要先介绍一下 JVM TI(JVM Tool Interface)。
JVM TI 是 JVM 提供的用于访问 JVM 各种状态的一套编程接口。基于 JVM TI 可以注册各种 JVM 事件钩子函数,当 JVM 事件发生时,触发钩子函数以对相应的 JVM 事件进行处理。关于 JVM TI 的详细文档,可以参考 JVMTM Tool Interface。
那么 Java Agent 可以理解为就是 JVM TI 的一种具体实现。关于 Java Agent,可以概括其特性如下。
是一个 jar 包;
无法独立运行;
(JDK1.5)可以在程序运行前被加载,加载后会调用到 Java Agent 提供的入口函数 premain(String agentArgs, Instrumentation inst);
(JDK1.6 开始)可以在程序运行中被加载,加载后会调用到 Java Agent 提供的入口函数 agentmain(String agentArgs, Instrumentation inst)。
如果想要 agentmain() 方法被调用,则需要将 Agent 程序 attach 到主进程的 JVM 上,这时就需要使用到 com.sun.tools.attach 包里提供的 Attach API,Agent 被 attach 到 JVM 后,agent 的 agentmain() 方法就会被调用。
最后说明一下 Java Agent 的入口函数中的类型为 Instrumentation 的参数。Instrument 是 JVM 提供的一套能够对 Java 代码进行插桩操作的服务能力,JDK1.5 的 Instrument 支持在 JVM 启动并加载类时修改类,Instrument 从 JDK1.6 开始支持在程序运行时修改类。Instrument 提供的重要方法如下所示。
public interface Instrumentation {
// ...
/**
* JDK1.5提供,注册一个{@link ClassFileTransformer}。
* 等同于addTransformer(transformer, false)。
*
* @param transformer {@link ClassFileTransformer}。
*/
void addTransformer(ClassFileTransformer transformer);
/**
* JDK1.6提供,注册一个{@link ClassFileTransformer}。
*
* @param transformer {@link ClassFileTransformer}。
* @param canRetransform false表示注册的{@link ClassFileTransformer}仅对首次加载的类生效,
* 即首次加载类时可以改变这个类的定义再完成加载;
* true表示注册的{@link ClassFileTransformer}可对已加载的类生效,即
* 可对已加载的类进行重定义并重加载,重加载重定义的类时会覆盖已加载的类。
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* JDK1.6提供,重定义并重加载传入的类。
*
* @param classes 需要重定义并重加载的类。
* @throws UnmodifiableClassException 传入的类无法被修改时抛出。
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// ...
}
复制代码
也就是可以向 Instrumentation 注册 ClassFileTransformer。
JDK1.5 时只能通过 addTransformer(ClassFileTransformer) 方法注册 ClassFileTransformer,此时每个类被加载到 JVM 中之前会调用到注册的 ClassFileTransformer 的 transform() 方法,并可以在其中先改变类定义后再将类加载到 JVM 中。
JDK1.6 开始提供了 addTransformer(ClassFileTransformer, boolean) 方法,当第二个参数传入 false 时,效果与 addTransformer(ClassFileTransformer) 方法一样,当第二个参数传入 true 时,那么此时注册的 ClassFileTransformer 除了在类被加载到 JVM 中之前会调用到,还会在 retransformClasses(Class<?>... classes) 方法调用时被调用到,也就是此时注册的 ClassFileTransformer 支持对通过 retransformClasses(Class<?>... classes) 方法传入的类进行重定义然后再重加载到 JVM 中。
二. 整体构思
首先,因为是超绝战损版,所以我们的方法耗时统计,伪代码可以表示如下。
public class TestDemo {
public void execute() {
// 记录开始时间
long beginTime = System.currentTimeMillis();
// 原方法方法体
// ...
// 记录结束时间
long endTime = System.currentTimeMillis();
// 记录执行耗时
long executeTime = endTime - beginTime;
// 超绝战损版打印
System.out.println(executeTime);
}
}
复制代码
其次,我们需要编写一个 Java Agent,且希望能够在程序运行时加载这个 Java Agent,所以编写的 Java Agent 需要提供入口函数 agentmain(String agentArgs, Instrumentation inst),此时 Java Agent 需
要通过 com.sun.tools.attach 包里提供的 Attach API 来加载并附加到主进程 JVM 上。
然后,在 Java Agent 中,我们需要初始化 ClassFileTransformer,然后将 ClassFileTransformer 注册到 Instrumentation,再然后获取到需要重定义的类并通过 Instrumentation 的 retransformClasses(Class<?>... classes) 方法将这些类传递到注册的 ClassFileTransformer 中。
接着,在我们自定义的 ClassFileTransformer 中,需要借助 Javassist 的能力,为相应的类添加方法耗时统计的代码片段,并完成重加载。
最后,还需要编写一个测试程序来验证我们的超绝战损版方法耗时打印工具的功能。
整体的一个流程示意图如下。
三. 方法耗时统计工具实现
现在开始代码实现。首先创建一个 Maven 工程,命名为 myagent-core,POM 文件如下所示。
<?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>com.lee.learn.agent</groupId>
<artifactId>myagent-core</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
</dependencies>
<build>
<finalName>myagent-core</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
复制代码
POM 文件中主要就是引入必须的 javassist 的依赖,以及通过打包插件将依赖打入 jar 包。
然后需要创建 src/main/resources/META-INF 目录,然后在其中创建 MANIFEST.MF 文件,内容如下所示。
Agent-Class: com.lee.learn.agent.core.MethodAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
复制代码
特别注意最后有一个空行。
现在开始编写 Java Agent 的代码。首先是自定义一个转换器,命名为 MTransformer,并实现 ClassFileTransformer 接口,代码现如下。
public class MTransformer implements ClassFileTransformer {
/**
* 目标类的类全限定名和类加载器的映射,用于筛选出需要重定义的类。
* 映射类型是:Map[类全限定名, 类加载器]。
*/
private final Map<String, ClassLoader> targetClassesMap = new HashMap<>();
/**
* 初始化时就需要传入目标类集合,并转换成映射关系。
*
* @param targetClasses 目标类的类对象集合。
*/
public MTransformer(List<Class<?>> targetClasses) {
targetClasses.forEach(targetClass ->
targetClassesMap.put(
targetClass.getName(),
targetClass.getClassLoader()));
}
/**
* 基于Javassist改造类方法,为每个方法添加打印执行耗时的逻辑。
*
* @param loader 需要改造的类的类加载器。
* @param className 需要改造的类的类全限定名。
* @param classBeingRedefined 有值时传入的就是正在被重定义的类的类对象,如果是类加载阶段那么传入为null。
* @param protectionDomain 改造类的保护域。
* @param classfileBuffer 类文件的输入字节缓冲区。
* @return 改造后的类文件字节缓存区,如果未执行改造,返回null。
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
byte[] byteCode = classfileBuffer;
// 类加载器+类路径才能完全确定一个类
// 基于类加载器和类路径进行目标类筛选
String targetClassName = className.replaceAll(PATH_SEP, REGEX_QUALIFIER);
if (targetClassesMap.get(targetClassName) == null
|| !targetClassesMap.get(targetClassName).equals(loader)) {
return byteCode;
}
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(targetClassName);
// 对目标类的所有方法都插入统计耗时逻辑
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod ctMethod : declaredMethods) {
// 插入统计耗时逻辑
transformMethod(ctMethod);
}
byteCode = ctClass.toBytecode();
ctClass.detach();
} catch (Exception e) {
e.printStackTrace();
}
return byteCode;
}
/**
* 为每个方法添加统计执行耗时的逻辑。
*
* @param ctMethod 详情见{@link CtMethod}。
*/
private void transformMethod(CtMethod ctMethod) throws Exception {
// 在方法内添加本地参数
ctMethod.addLocalVariable("beginTime", CtClass.longType);
ctMethod.addLocalVariable("endTime", CtClass.longType);
ctMethod.addLocalVariable("executeTime", CtClass.longType);
// 方法体之前添加统计开始时间的代码
ctMethod.insertBefore("beginTime = System.currentTimeMillis();");
// 方法体结束位置添加获取结束时间并计算执行耗时的代码
String endCode = "endTime = System.currentTimeMillis();" +
"executeTime = endTime - beginTime;" +
"System.out.println(executeTime);";
ctMethod.insertAfter(endCode);
}
}
复制代码
在 MTransformer 的构造函数中,需要传入目标类的类对象的集合,目的就是做到动态的控制对哪些类添加方法耗时统计的逻辑。
最后定义 Java Agent 的主体类,命名为 MethodAgent,代码如下所示。
public class MethodAgent {
public static final String REGEX_QUALIFIER = "\\.";
private static final String REGEX_CLASS_SUFFIX = "\\.class";
private static final String QUALIFIER = ".";
private static final String CLASS_SUFFIX = ".class";
public static final String PATH_SEP = "/";
private static final String SEP = ",";
private static final String EMPTY = "";
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
if (agentArgs == null) {
return;
}
// 期望agentArgs传入的是一个以英文逗号分隔的多个路径
String[] basePackages = agentArgs.split(SEP);
List<Class<?>> targetClasses = new ArrayList<>();
for (String basePackage : basePackages) {
try {
// 获取到传入路径下的所有类的类对象
findClasses(basePackage, targetClasses, MethodAgent.class.getClassLoader());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
// 基于获取到的类的类对象集合创建MTransformer,并向Instrumentation注册MTransformer转换器
// 需要指定canRetransform为true,否则下面调用的retransformClasses()方法会不生效
instrumentation.addTransformer(new MTransformer(targetClasses), true);
try {
// 将所有目标类通过retransformClasses()方法传递到MTransformer转换器完成转换
instrumentation.retransformClasses(targetClasses.toArray(new Class<?>[0]));
} catch (Exception e) {
e.printStackTrace();
}
}
private static void findClasses(String basePackage, List<Class<?>> clazzList, ClassLoader classLoader)
throws IOException, ClassNotFoundException {
Enumeration<URL> resources = classLoader.getResources(basePackage.replaceAll(REGEX_QUALIFIER, PATH_SEP));
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String[] fileNames = new File(url.getPath()).list();
if (fileNames == null || fileNames.length == 0) {
return;
}
for (String fileName : fileNames) {
if (!fileName.endsWith(CLASS_SUFFIX)) {
findClasses(basePackage + QUALIFIER + fileName, clazzList, classLoader);
} else {
clazzList.add(Class.forName(basePackage + QUALIFIER + fileName.replaceAll(REGEX_CLASS_SUFFIX, EMPTY)));
}
}
}
}
}
复制代码
至此 Java Agent 就编写完毕,整个的工程目录如下所示。
可以先将 Java Agent 进行打包,并将得到的 jar 包放在磁盘的某个路径下,这里就放在 D 盘的根路径下(D:\myagent-core-jar-with-dependencies.jar)。
下面编写测试工程。首先创建 Maven 工程,命名为 myagent-local-test,POM 文件如下所示。
<?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>com.lee.learn.agent</groupId>
<artifactId>myagent-local-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
</project>
复制代码
主要就是引入 sun 的工具包。
然后创建两个测试目标类,InnerTask 位于
com.lee.learn.agent.test.inner 包路径下,OuterTask 位于
com.lee.learn.agent.test.outter 包路径下,实现如下所示。
public class InnerTask {
public void execute() {
System.out.println("Begin to execute inner task.");
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
}
public void close() {
System.out.println("Begin to close inner task.");
LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
}
}
public class OuterTask {
public void execute() {
System.out.println("Start to execute outer task.");
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
}
public void close() {
System.out.println("Start to close outer task.");
LockSupport.parkNanos(1000 * 1000 * 1000 * 3L);
}
}
复制代码
然后就是主测试方法,如下所示。
public class MainTest {
private static final String agentPath = "D:\\myagent-core-jar-with-dependencies.jar";
public static void main(String[] args) {
loadAgent();
InnerTask innerTask = new InnerTask();
innerTask.execute();
innerTask.close();
OuterTask outerTask = new OuterTask();
outerTask.execute();
outerTask.close();
}
private static void loadAgent() {
try {
// 获取主进程Id
String jvmId = getJvmId();
// Attach到主进程
VirtualMachine virtualMachine = VirtualMachine.attach(jvmId);
// 加载Java Agent,并指定包路径
virtualMachine.loadAgent(agentPath, "com.lee.learn.agent.test.inner");
virtualMachine.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String getJvmId() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
String runtimeMXBeanName = runtimeMXBean.getName();
return runtimeMXBeanName.split("@")[0];
}
}
复制代码
因为事先已经将 Java Agent 的 jar 包放在了 D 盘根路径下,所以在测试程序中 attach 到主进程中后,直接通过 jar 包加载 Java Agent。
测试工程目录结构如下所示。
运行测试程序,打印如下所示。
四. 方法耗时统计工具的 Springboot 的 starter 包实现
第三节中的方法耗时统计工具,功能是实现了,并且主要就是依靠一个 Java Agent 的 jar 包,但是实在是太简陋了,作为超绝战损版也完全不能看,缺点如下。
Java Agent 的 jar 包需要通过某种手段才能让应用程序找得到。例如容器中的一个应用,要使用这个 Java Agent,首先要做的事情就是下载 jar 包,然后拷贝到容器中的某个路径下;
对用户代码产生了侵入。在测试程序中,编写了代码并调用了 com.sun.tools.attach 包的 VirtualMachine 的相关 API 才实现了 attach 主进程以及加载 Java Agent,这在实际使用中,大家肯定都是不愿意做这个事情的。
鉴于第三节中的做法实在是不优雅,所以本节会编写一个方法耗时统计的 starter 包,只需要在 Springboot 工程中引用这个包,然后做少量配置,就能够实现和第三节一样的方法耗时统计效果。
整体会创建三个工程,如下所示。
myagent-package 工程。该工程仅需要做一件事情,就是存放 Java Agent 的 jar 包;
myagent-starter 工程。starter 包,主要完成的事情就是完成 Java Agent 的加载;
myagent-starter-test 工程。测试工程。
主要的做法和部分代码,参考了 Arthas 的 Springboot 的 starter 包 arthas-spring-boot-starter 的实现。
1. myagent-package 工程
创建一个 Maven 工程,命名为 myagent-package,然后将 Java Agent 的 jar 包打成 zip 包并放在 myagent-package 工程的 src/main/resources 目录下,如下所示。
最后将 myagent-package 通过 install 安装到本地仓库。
2. myagent-starter 工程
创建一个 Maven 工程,POM 文件如下所示。
<?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>
<parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.6</version>
</parent>
<groupId>com.lee.learn.agent</groupId>
<artifactId>myagent-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>com.lee.learn.agent.package</groupId>
<artifactId>myagent-package</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>zt-zip</artifactId>
<version>1.14</version>
</dependency>
</dependencies>
</project>
复制代码
上述 POM 文件中引入的 zt-zip 是一个 zip 包工具,然后最关键的就是需要引入 myagent-package 的依赖,Java Agent 的 jar 包的压缩包就在这个依赖包中。
myagent-starter 的核心思路就是基于 Springboot 的 SPI 机制注册一个 ApplicationListener 监听器,监听的事件是 ApplicationEnvironmentPreparedEvent,也就是在外部配置加载完毕后就开始加载 Java Agent。
现在先创建
src/main/resources/META-INF 目录,然后创建 spring.factories 文件,内容如下所示。
org.springframework.context.ApplicationListener=\
com.learn.agent.starter.MyAgentApplicationListener
复制代码
自定义的事件监听器
MyAgentApplicationListener 实现如下所示。
public class MyAgentApplicationListener implements GenericApplicationListener {
private static final Class<?>[] EVENT_TYPES = {ApplicationEnvironmentPreparedEvent.class};
@Override
public boolean supportsEventType(ResolvableType eventType) {
return isAssignableFrom(eventType.getRawClass(), EVENT_TYPES);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();
try {
new MyAgentLoader(environment).load();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {
if (type != null) {
for (Class<?> supportedType : supportedTypes) {
if (supportedType.isAssignableFrom(type)) {
return true;
}
}
}
return false;
}
}
复制代码
监听到
ApplicationEnvironmentPreparedEvent 事件后,就会创建一个 MyAgentLoader 加载器并调用其 load() 方法。Java Agent 的加载器 MyAgentLoader 实现如下。
public class MyAgentLoader {
private static final int TEMP_DIR_ATTEMPTS = 10000;
private static final String AGENT_ZIP_NAME = "myagent-core-jar-with-dependencies.zip";
private static final String AGENT_JAR_NAME = "myagent-core-jar-with-dependencies.jar";
private final ConfigurableEnvironment environment;
public MyAgentLoader(ConfigurableEnvironment environment) {
this.environment = environment;
}
public void load() throws IOException {
// 创建临时目录用于存放agent的jar包
File tempDir = createTempDir();
// 解压得到agent的jar包并放到临时目录
URL agentJarUrl = this.getClass().getClassLoader().getResource(AGENT_ZIP_NAME);
ZipUtil.unpack(agentJarUrl.openStream(), tempDir);
// 拿到主进程Id
String jvmId = getJvmId();
String basePackage = environment.getProperty("myagent.basepackage");
try {
// Attach到主进程
VirtualMachine virtualMachine = VirtualMachine.attach(jvmId);
// 加载Java Agent,并传入包路径
virtualMachine.loadAgent(new File(tempDir.getAbsolutePath(), AGENT_JAR_NAME).getAbsolutePath(), basePackage);
virtualMachine.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
private static File createTempDir() {
File baseDir = new File(System.getProperty("java.io.tmpdir"));
String baseName = "myagent-" + System.currentTimeMillis() + "-";
for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
File tempDir = new File(baseDir, baseName + counter);
if (tempDir.mkdir()) {
return tempDir;
}
}
throw new IllegalStateException("Failed to create directory within " + TEMP_DIR_ATTEMPTS + " attempts (tried "
+ baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
}
private static String getJvmId() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
String runtimeMXBeanName = runtimeMXBean.getName();
return runtimeMXBeanName.split("@")[0];
}
}
复制代码
MyAgentLoader#load 方法的主要思路如下。
创建用于存放 Java Agent 的 jar 包的临时目录;
从 classpath 下找到 Java Agent 的 zip 包;
将 Java Agent 的 zip 包解压到刚创建出来的临时目录中;
拿到主进程 Id;
从 Environment 中拿到配置的目标包路径;
基于 VirtualMachine 附加到主进程上;
加载 Java Agent,并传入目标包路径。
至此 starter 包就编写完毕。myagent-starter 工程的目录结构如下所示。
最后还需要将 myagent-starter 通过 install 安装到本地仓库。
3. myagent-starter-test 工程
现在开始编写测试工程并完成测试,测试工程的目录结构如下所示。
是一个简单的三层架构,首先 POM 文件如下所示。
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.7.6</version>
</parent>
<groupId>com.lee.learn.agent</groupId>
<artifactId>myagent-starter-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.leanr.agent.starter</groupId>
<artifactId>myagent-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
复制代码
然后 MyController,MyService 和 MyDao 的实现如下。
@RestController
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/testagent")
public String testAgent() {
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
myService.testAgent();
System.out.println("MyController executed.");
return "Test Agent Success.";
}
}
@Service
public class MyService {
@Autowired
private MyDao myDao;
public void testAgent() {
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
myDao.testAgent();
System.out.println("MyService executed.");
}
}
@Repository
public class MyDao {
public void testAgent() {
LockSupport.parkNanos(1000 * 1000 * 1000 * 2L);
System.out.println("MyDao executed.");
}
}
复制代码
也就是模拟每个方法会耗时 2 秒。最后编写配置文件,如下所示。
myagent:
basepackage: com.lee.learn.agent.startertest.controller,com.lee.learn.agent.startertest.dao
复制代码
配置仅对 controller 和 dao 包下的类进行方法耗时统计。
最后启动 Springboot 程序,并调用 MyController 接口,打印如下。
方法耗时统计确实只针对 controller 和 dao 包生效了,至此测试完毕。
总结
Java Agent 就是一个无法独立运行的 jar 包,其加载时机可以是程序运行前和程序运行中,也就是基于 Java Agent 可以实现在程序运行前和程序运行中来动态的修改类。
方法耗时统计,简单的思路就是使用切面去切,首先想到的就是使用 Spring 的 AOP 来切,但是 Spring 的 AOP 都知道是基于动态代理,但是无论是 JDK 动态代理,还是 CGLIB 动态代理,都有其局限性(貌似 AspectJ 可行,但这不是本文的重点),不是所有类都能切,所以本文采取的思路就是基于 Java Agent 再结合 Javassist 的能力,完成向目标类的方法插入方法耗时统计的逻辑。
一个 Java Agent 的 jar 包,是一个很精致的 jar 包,但是有些时候想要这个 jar 包被加载,还真有点头疼,主要是放哪里怎么解决,所以提供一个 Springboot 的 starter 包貌似是一个很好的解决思路,只需要在程序中引入提供的 starter 包,那么我们的程序最终无论是虚机部署,还是容器部署,我们都能拿到 Java Agent 并加载。
本文的方法耗时统计,之所以称为战损版,是因为仅仅做了耗时的一个打印,但是真正有用的是啥,那就是能够通过链路 Id 将方法调用链路以及耗时串起来,但是这也不是本文的重点。
评论