前言
本篇文章将实现一个超绝战损版的基于Java Agent的方法耗时统计工具。
整体内容分为:
- Java Agent原理简析;
- 方法耗时统计工具实现;
- 方法耗时工具的Springboot的starter包实现。
正文
一. Java Agent原理简析
理解啥是Java Agent前,需要先介绍一下JVM TI(JVM Tool Interface)。
JVM TI是JVM提供的用于访问JVM各种状态的一套编程接口。基于JVM TI可以注册各种JVM事件钩子函数,当JVM事件发生时,触发钩子函数以对相应的JVM事件进行处理。
那么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.