Posted in

探讨 JavaAgent 原理,实现方法执行耗时统计_AI阅读总结 — 包阅AI

包阅导读总结

1.

“`

JavaAgent、字节码修改、方法耗时统计、ClassFileTransformer、Instrumentation

“`

2.

本文主要介绍了 JavaAgent 的原理、实现方法执行耗时统计的实践。指出其本质是特殊的 jar 包,能在应用底层实现数据采集。通过案例详细阐述了其工作原理和实现步骤,包括实现接口、注册、打包配置等。

3.

– 前言

– 介绍常见的应用层监控方式,提出 JavaAgent 可深入底层采集数据

– JavaAgent 是什么

– 本质是特殊的 jar 包,需附加到目标 JVM 进程发挥功能

– 以 IntelliJ IDEA 调试为例说明其作用

– JavaAgent 工作原理

– 归结为加载和执行阶段,通过-javaagent 命令行参数和 premain 方法实现

– 借助 Instrumentation API 和 ClassFileTransformer 接口修改字节码

– 实践 JavaAgent

– 实现 ClassFileTransformer 接口,重写 transform 方法进行字节码修改

– 代理入口类包含 premain 方法并完成 ClassFileTransformer 注册

– 配置 MANIFEST.MF 文件,打包成 Jar 包,通过 -javaagent 启动应用

– 总结

– 分析 JavaAgent 原理和使用方式,总结编写 JavaAgent 的步骤

思维导图:

文章地址:https://mp.weixin.qq.com/s/sRh7ZjBk1QuPX9OEe7hr_g

文章来源:mp.weixin.qq.com

作者:毅航

发布时间:2024/8/7 10:51

语言:中文

总字数:2944字

预计阅读时间:12分钟

评分:84分

标签:JavaAgent,性能监控,字节码修改,JVM,代码优化


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

前言

为了能监控程序执行信息,我们通常会借助SpringAOP机制或依靠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 AgentJVM 启动时附加一些特殊的逻辑,这些逻辑允许 IntelliJ IDEA 控制和监视 JVM的执行过程。具体来看, IntelliJ IDEA在启动 JVM 时通过 -javaagent 参数加载一个特定的 JavaAgent。而这个 JavaAgent 实际上是由 IntelliJ IDEA 自身,当相关的JavaAgen加载成功后,相关的JavaAgent即可在类加载之前对字节码进行修改,从而实现调试功能。例如,断点管理、单步执行、变量追踪、堆栈跟踪等调试操作。

事实上, JavaAgentJDK提供给开发者的一种可以对已有class代码进行运行时注入修改的能力。借助JavaAgent技术我们可以对特定的类进行字节码修改, 从而在方法执行前后注入特定的逻辑,以实现对类执行的增强和修改。

知晓了JavaAgent的基本概念后,我们接下来便对JavaAgent的工作原理进行分析。

JavaAgent工作原理

JavaAgent的使用基本可以归结加载执行两个阶段。具体来看,JavaAgent需要通过-javaagent 命令行参数来实现对JavaAgent加载。而-javaagent可接收一个指向JavaAgent的路径,其指向 JavaAgentJAR 文件。进一步,对于JavaAgent的启动而言,其内部需定义一个 premain 方法,作为JavaAgent的主要入口。其中premain方法签名如下:


publicstaticvoidpremain(StringagentArgs,
Instrumentationinst)

不难发现,对于premain方法而言其主会通过传入Instrumentation 对象,来保证Java AgentInstrumentation API的各种方法的方法文,例如:通过Instrumentation APIaddTransformer从而添加一个 ClassFileTransformer进而实现来转换类的字节码。

ClassFileTransformer 则是Java中用于修改类文件字节码的接口。其主要用于在类加载到JVM时对类的字节码进行修改。对于ClassFileTransformer 而言,其只有一个方法transform,该方法允许你在类被加载到JVM之前对其字节码进行修改。方法签名如下:


publicbyte[]transform(ClassLoaderloader,
StringclassName,Class<?>classBeingRedefined,
ProtectionDomainprotectionDomain,
byte[]classfileBuffer)

transform方法中,你可以使用字节码操作库(如 ASM、Byte Buddy 等)来修改字节码,例如插入新的指令或方法调用。

「换言之,如果要想期待给类的Class文件添加一下自定义逻辑的话,我们需要借助ClassFileTransformer来完成。」

实践JavaAgent

明白了JavaAgent的原理后,接下来我们便来自己Coding一个统计指定方法耗时的JavaAgent

具体来看,如果我们要手动编写一个JavaAgent大致需要如下几步

  1. 实现 ClassFileTransformer接口,重写transform方法

正如之前介绍的那样, 如果我们想对字节码文件进行修改,我们需要借助 ClassFileTransformer 接口的 transform 方法,从而保证可以在类加载到 JVM之前对其字节码进行修改。

如果我们要对方法执行耗时进行统计的话,最简单的方式无异于在方法开始前进行计时,当方法结束时再次进行计时,两个时间相减即为方法执行所耗时长。因此transform方法的逻辑如下:

publicclassCostTransformerimplementsClassFileTransformer{


privatefinalStringtargetClassNameSuffix="UserController";

@Override
publicbyte[]transform(ClassLoaderloader,StringclassName,Class<?>classBeingRedefined,
ProtectionDomainprotectionDomain,byte[]classfileBuffer){
//这里我们限制下,只针对目标包下进行耗时统计
if(!className.contains(targetClassNameSuffix)){
returnclassfileBuffer;
}
CtClasscl=null;
try{
ClassPoolclassPool=newClassPool();
classPool.appendSystemPath();
CtClassctClass=classPool.getCtClass("com.example.controller.UserController");
CtMethodmethod=ctClass.getDeclaredMethods("testCostTime")[0];
//所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量
method.addLocalVariable("start",CtClass.longType);
method.insertBefore("start=System.currentTimeMillis();");
StringmethodName=method.getLongName();
method.insertAfter("System.out.println(\"监控信息(方法执行耗时):"+methodName+"cost:\"+(System"+
".currentTimeMillis()-start));");
returnctClass.toBytecode();
}catch(Exceptione){
e.printStackTrace();
}
returnclassfileBuffer;
}
}

在上述代码中,我们通过method.insertBefore在方法执行前插入一个start变量,用户记录方法执行开始时间。然后,借助method.insertAfter在方法执行末尾插入耗时计算逻辑。

对于JavaAgent而言其代理入口类通常包含 premainagentmain 方法,并在相关方法内完成 ClassFileTransformer的注册。因此,为了确保我们编写的ClassFileTransformer能成功被JavaAgent所加载,所以我们需要在premain 方法内部完成相关注册。相关逻辑如下:

publicclassMyAgent{

publicstaticvoidpremain(StringagentArgs,Instrumentationinst){

inst.addTransformer(newCostTransformer());
}
}

(注:agentmain通常是在JavaAgent附着启动时所需,本文我们主要介绍JavaAgent启动时加载的方式,即我们主要介绍 premain的使用 )

通常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-timerjar

(应用启动后,成功记录出UserControllertestCostTime方法执行时长)

注:本次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 接口以修改类的字节码。

  • 在代理类的 premainagentmain 方法中注册 ClassFileTransformer

  • 打包代理JAR,并在 MANIFEST.MF 中指定代理类。

  • 使用 -javaagent 选项启动 Java应用程序,或使用Java Attach API在运行时附加代理。