探讨 JavaAgent原理,实现方法执行耗时统计
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜
前言
为了能监控程序执行信息,我们通常会借助Spring
的AOP
机制或依靠SpringMVC
中的拦截器(Interceptor)
来在请求进入到应用层时进行拦截,从而实现程序执行信息的采集。虽然上述实现方式各有不同,但上述技术有一个共同点,即上述方式有一个共同点那便是其都是应用层
的角度实现切入,从而实现数据信息的收集。
而对于数据信息采集而言,越是深入到应用底层越能实现对数据采集的精细化采集和控制,而对于底层数据信息的收集在Java
应用程序汇总,完全可以借助JavaAgent
来进行采集。
JavaAgent
是什么
JavaAgent
也称 Java
代理,其本质是一个特殊的 jar
包。但与普通jar
包不同的是JavaAgent
不是独立运行的,而是需要附加到目标 JVM
进程中以发挥其功能。
进一步来看,当应用启动时 JavaAgent
会附着到目标应用的JVM
上,从而实现对JVM
运行数据的采集工作。而JavaAgent
在这里其实扮演了一个中间人的角色。从目标 JVM
的角度来看,JavaAgent
就像是一个代理,帮助我们获取所需的运行指标。
这样讲可能比较晦涩,接下来让我们通过一个简单来进行说明。例如,当我们使用 IntelliJ IDEA
的调试功能时,当我们在 IntelliJ IDEA
中启动调试模式时,实际上 IntelliJ IDEA
会使用 JavaAgent
来增强 JVM
的行为,以实现调试功能。
具体来看,IntelliJ IDEA
会通过 Java Agent
在 JVM
启动时附加一些特殊的逻辑,这些逻辑允许 IntelliJ IDEA
控制和监视 JVM
的执行过程。具体来看, IntelliJ IDEA
在启动 JVM
时通过 -javaagent
参数加载一个特定的 JavaAgent
。而这个 JavaAgent
实际上是由 IntelliJ IDEA
自身,当相关的JavaAgen
加载成功后,相关的JavaAgent
即可在类加载之前对字节码进行修改,从而实现调试功能。例如,断点管理、单步执行、变量追踪、堆栈跟踪等调试
操作。
事实上, JavaAgent
是JDK
提供给开发者的一种可以对已有class
代码进行运行时注入修改的能力。 借助JavaAgent
技术我们可以对特定的类进行字节码修改, 从而在方法执行前后注入特定的逻辑,以实现对类执行的增强和修改。
知晓了JavaAgent
的基本概念后,我们接下来便对JavaAgent
的工作原理进行分析。
JavaAgent
工作原理
JavaAgent
的使用基本可以归结加载
和执行
两个阶段。具体来看,JavaAgent
需要通过-javaagent
命令行参数来实现对JavaAgent
加载。而-javaagent
可接收一个指向JavaAgent
的路径,其指向 JavaAgent
的 JAR
文件。 进一步,对于JavaAgent
的启动而言,其内部需定义一个 premain
方法,作为JavaAgent
的主要入口。 其中premain
方法签名如下:
public static void premain(String agentArgs,
Instrumentation inst)
不难发现,对于premain
方法而言其主会通过传入 Instrumentation
对象,来保证Java Agent
对Instrumentation API
的各种方法的方法文,例如:通过Instrumentation API
的addTransformer
从而添加一个 ClassFileTransformer
进而实现来转换类的字节码。
而ClassFileTransformer
则是Java
中用于修改类文件字节码的接口。其主要用于在类加载到JVM
时对类的字节码进行修改。对于ClassFileTransformer
而言,其只有一个方法transform
,该方法允许你在类被加载到JVM
之前对其字节码进行修改。方法签名如下:
public byte[] transform(ClassLoader loader,
String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
在 transform
方法中,你可以使用字节码操作库(如 ASM、Byte Buddy
等)来修改字节码,例如插入新的指令或方法调用。
换言之,如果要想期待给类的Class
文件添加一下自定义逻辑的话,我们需要借助ClassFileTransformer
来完成。
实践JavaAgent
明白了JavaAgent
的原理后,接下来我们便来自己Coding
一个统计指定方法耗时的JavaAgent
。
具体来看,如果我们要手动编写一个JavaAgent
大致需要如下几步
- 实现
ClassFileTransformer
接口,重写transform
方法
正如之前介绍的那样, 如果我们想对字节码文件进行修改,我们需要借助 ClassFileTransformer
接口的 transform
方法,从而保证可以在类加载到 JVM
之前对其字节码进行修改。
如果我们要对方法执行耗时进行统计的话,最简单的方式无异于在方法开始前进行计时,当方法结束时再次进行计时,两个时间相减即为方法执行所耗时长。因此transform
方法的逻辑如下:
public class CostTransformer implements ClassFileTransformer {
private final String targetClassNameSuffix = "UserController";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 这里我们限制下,只针对目标包下进行耗时统计
if (!className.contains(targetClassNameSuffix)) {
return classfileBuffer;
}
CtClass cl = null;
try {
ClassPool classPool = new ClassPool();
classPool.appendSystemPath();
CtClass ctClass = classPool.getCtClass("com.example.controller.UserController");
CtMethod method = ctClass.getDeclaredMethods("testCostTime")[0];
// 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量
method.addLocalVariable("start", CtClass.longType);
method.insertBefore("start = System.currentTimeMillis();");
String methodName = method.getLongName();
method.insertAfter("System.out.println(\"监控信息(方法执行耗时):" + methodName + " cost: \" + (System" +
".currentTimeMillis() - start));");
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
在上述代码中,我们通过method.insertBefore
在方法执行前插入一个start
变量,用户记录方法执行开始时间。然后,借助method.insertAfter
在方法执行末尾插入耗时计算逻辑。
- 编写代理入口类
对于JavaAgent
而言其代理入口类通常包含 premain
或 agentmain
方法,并在相关方法内完成 ClassFileTransformer
的注册。因此,为了确保我们编写的ClassFileTransformer
能成功被JavaAgent
所加载,所以我们需要在premain
方法内部完成相关注册。相关逻辑如下:
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new CostTransformer());
}
}
(注: agentmain
通常是在JavaAgent
附着启动时所需,本文我们主要介绍JavaAgent
启动时加载的方式,即我们主要介绍 premain
的使用 )
- 配置
Jar
信息,完成Jar
打包
通常JavaAgent
都是通过Jar
的形式进行运行,因此我们需要将我们上述的代码打包成一个Jar
包,在打包之前我们需要配置一个 MANIFEST.MF
文件以指定JavaAgent
的启动类:
Manifest-Version: 1.0
Premain-Class: MyAgent
Agent-Class: MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
至此,我们整个项目接口如下所示:
然后借助Maven
来生成相应的Jar
包,本次笔者这里打成的Jar
包名称为exec-timer
。完成Jar
的打包后,即可在应用启动时指定加载相应路径下的Jar
包,从而完成JavaAgent
的启动。具体如下:
(配置VM
相关参数,指定加载target
路径下的exec-timer
的jar
)
(应用启动后,成功记录出UserController
下testCostTime
方法执行时长)
注:本次Coding
我们所使用的依赖如下:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>5.0.3</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
总结
本文主要对JavaAgent
的原理和使用方式进行了分析介绍,并结合具体案例对JavaAgent
的使用进行详细的分析,具体来看,如果我们要编写一个JavaAgent
具体需要完成如下步骤:
-
实现
ClassFileTransformer
接口以修改类的字节码。 -
在代理类的
premain
或agentmain
方法中注册ClassFileTransformer
。 -
打包代理
JAR
,并在MANIFEST.MF
中指定代理类。 -
使用
-javaagent
选项启动Java
应用程序,或使用Java Attach API
在运行时附加代理。
转载自:https://juejin.cn/post/7398479291263582244