likes
comments
collection
share

探讨 JavaAgent原理,实现方法执行耗时统计

作者站长头像
站长
· 阅读数 18

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜


前言

为了能监控程序执行信息,我们通常会借助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 Agent JVM 启动时附加一些特殊的逻辑,这些逻辑允许 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方法签名如下:


public static void premain(String agentArgs, 
                    Instrumentation inst) 

不难发现,对于premain方法而言其主会通过传入 Instrumentation 对象,来保证Java Agent Instrumentation API的各种方法的方法文,例如:通过Instrumentation APIaddTransformer从而添加一个 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大致需要如下几步

  1. 实现 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在方法执行末尾插入耗时计算逻辑。

  1. 编写代理入口类

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

public class MyAgent {

    public static void premain(String agentArgs, Instrumentation inst) {

        inst.addTransformer(new CostTransformer());
    }
}

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

  1. 配置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

至此,我们整个项目接口如下所示:

探讨 JavaAgent原理,实现方法执行耗时统计

然后借助Maven来生成相应的Jar包,本次笔者这里打成的Jar包名称为exec-timer。完成Jar的打包后,即可在应用启动时指定加载相应路径下的Jar包,从而完成JavaAgent的启动。具体如下:

探讨 JavaAgent原理,实现方法执行耗时统计

(配置VM相关参数,指定加载target路径下的exec-timerjar

探讨 JavaAgent原理,实现方法执行耗时统计

(应用启动后,成功记录出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在运行时附加代理。

转载自:https://juejin.cn/post/7398479291263582244
评论
请登录