专栏首页小灰灰手把手教你实现一个方法耗时统计的 java agent

手把手教你实现一个方法耗时统计的 java agent

1. 基本姿势点

上面两节虽然手把手教你实现了一个 hello world 版 agent,然而实际上对 java agent 依然是一脸茫然,所以我们得先补齐一下基础知识

首先来看 agent 的两个方法中的参数 Instrumentation,我们先看一下它的接口定义

/**
 * 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
 * Transformer可以直接对类的字节码byte[]进行修改
 */
void addTransformer(ClassFileTransformer transformer);

/**
 * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
 * retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
 */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

/**
 * 获取一个对象的大小
 */
long getObjectSize(Object objectToSize);

/**
 * 将一个jar加入到bootstrap classloader的 classpath里
 */
void appendToBootstrapClassLoaderSearch(JarFile jarfile);

/**
 * 获取当前被JVM加载的所有类对象
 */
Class[] getAllLoadedClasses();

前面两个方法比较重要,addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。

通过上面的描述,可知

  • 可以通过Transformer修改类
  • 类加载时,会被触发 Transformer 拦截

2. 实现

我们需要统计方法耗时,所以想到的就是在方法的执行前,记录一个时间,执行完之后统计一下时间差,即为耗时

直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码

实现自定义的ClassFileTransformer,代码如下

public class CostTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        // 这里我们限制下,只针对目标包下进行耗时统计
        if (!className.startsWith("com/git/hui/java/")) {
            return classfileBuffer;
        }

        CtClass cl = null;
        try {
            ClassPool classPool = ClassPool.getDefault();
            cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

            for (CtMethod method : cl.getDeclaredMethods()) {
                // 所有方法,统计耗时;请注意,需要通过`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));");
            }

            byte[] transformed = cl.toBytecode();
            return transformed;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

然后稍微改一下 agent

/**
 * Created by @author yihui in 16:39 20/3/15.
 */
public class SimpleAgent {

    /**
     * jvm 参数形式启动,运行此方法
     *
     * manifest需要配置属性Premain-Class
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
        customLogic(inst);
    }

    /**
     * 动态 attach 方式启动,运行此方法
     *
     * manifest需要配置属性Agent-Class
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
        customLogic(inst);
    }

    /**
     * 统计方法耗时
     *
     * @param inst
     */
    private static void customLogic(Instrumentation inst) {
        inst.addTransformer(new CostTransformer(), true);
    }
}

到此 agent 完毕,打包和上面的过程一样,接下来进入测试环节

创建一个 DemoClz, 里面两个方法

public class DemoClz {

    public int print(int i) {
        System.out.println("i: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 2;
    }

    public int count(int i) {
        System.out.println("cnt: " + i);
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 1;
    }
}

然后对应的 main 方法如下

public class BaseMain {
    public static void main(String[] args) throws InterruptedException {
        DemoClz demoClz = new DemoClz();
        int cnt = 0;
        for (int i = 0; i < 20; i++) {
            if (++cnt % 2 == 0) {
                i = demoClz.print(i);
            } else {
                i = demoClz.count(i);
            }
        }
    }
}

选择 jvm 参数指定 agent 方式运行(具体操作和上面一样),输出如下

虽然我们的应用程序中并没有方法的耗时统计,但是最终的输出却完美的打印了每个方法的调用耗时,实现了无侵入的耗时统计功能

到这里本文的 java agent 的扫盲 + 实战(开发一个方法耗时统计)都已经完成了,是否就宣告着可以小结了,并不是,下面介绍一下在实现上面的 demo 过程中遇到的一个问题

3. Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame

在演示方法耗时的 agent 的示例中,并没有借助最开始的测试用例,而是新建了一个DemoClz来做的,那么为什么这样选择呢,如果直接用第二节的测试用例会怎样呢?

public class BaseMain {
    public int print(int i) {
        System.out.println("i: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 2;
    }

    public void run() {
        int i = 1;
        while (true) {
            i = print(i);
        }
    }

    public static void main(String[] args) {
        BaseMain main = new BaseMain();
        main.run();
}

依然通过 jvm 参数指定 agent 的方式,运行上面的代码,会发现抛异常,无法正常运行了

指出了在 run 方法这里,存在字节码的错误,我们统计耗时的 Agent,主要就是在方法开始前和结束后各自新增了一行代码,我们直接补充在 run 方法中,则相当于下面的代码

上面的提示很明显的告诉了,最后一行语句永远不可能达到,编译就存在异常了;那么问题来了,作为一个 java agent 的提供者,我哪知道使用者有没有写这种死循环的方法,如果应用中有这么个死循环的任务存在,把我的 agent 一挂载上去,导致应用都起不来,这个锅算谁的????

下面提供解决方案,也很简单,在 jvm 参数中,添加一个-noverify (请注意不同的 jdk 版本,参数可能不一样,我的本地是 jdk8,用这个参数;如果是 jdk7 可以试一下-XX:-UseSplitVerifier)

在 IDEA 开发环境下,如下配置即可

再次运行,正常了

4. 小结

本篇为实战项目,首先明确方法参数Instrumentation它的接口定义,通过它来实现 java 字节码的修改

我们通过实现自定义的ClassFileTransformer,借助 javassist 来修改字节码,为每个方法的第一行和最后一行注入耗时统计的代码,从而实现方法耗时统计

最后留一个小问题,上面的实现中,当方法内部抛出异常时,我们注入的最后一行统计耗时会不会如期输出,如果不会,应该怎么修改,欢迎各位大佬留言指出解决方案

(具体解决方案可以在源码中获取哦,还有配套的测试 case,求支持,求赞,求关注 ❀)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • cocos2dx-v3.5 2048(三):菜单实现

    前言 ---- 本节主要包括菜单栏的绘制以及添加触发事件,菜单栏又分为两级,如下面两张图,当点击set时,出现模式选择的菜单项。这里主要利用到了 MenuIt...

    一灰灰blog
  • SpringBoot基础篇AOP之高级使用技能

    前面一文,主要介绍的是根据正则表达式来拦截对应的方法,接下来演示下如何通过注解的方式来拦截目标方法,实现也比较简单

    一灰灰blog
  • IDEA + maven 零基础构建 java agent 项目

    Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的;虽说在实...

    一灰灰blog
  • 记使用WKWebView修改user-agent在iOS 12踩的一个坑

    随着摒弃了对iOS老系统的支持,项目也开始逐步开始转向WKWebView,本想着新系统应该能填一些WKWebView的坑,结果发现还是还是坑不断,这次在iOS1...

    iminder
  • 【Github】Chinese-poetry: 最全中华古诗词数据库

    上次我们玩了一下自动作诗机:"自动作诗机"上线,代码和数据都是公开的,基于该项目下自带的数据和模型。不过这方面还有一个诗词数据更全的Github项目:chine...

    AINLP
  • 【Github】Chinese-poetry: 最全中华古诗词数据库

    最近对自然语言生成或者文本自动生成技术比较感兴趣,做了一些调研,作为自然语言处理领域的难题之一,个人一直觉得自然语言生成(NLG)是最难的,虽然这一两年动辄会看...

    代码医生工作室
  • 你应该懂的管理学十大定理

    用户1756920
  • python接口自动化(二十四)--unittest断言——中(详解)

      上一篇通过简单的案例给小伙伴们介绍了一下unittest断言,这篇我们将通过结合和围绕实际的工作来进行unittest的断言。这里以获取城市天气预报的接口为...

    北京-宏哥
  • 数字文旅周报61期 | 江西省与腾讯签订战略合作协议,助推江西文旅产业转型升级

    ? ? 1.江西省与腾讯签订战略合作协议,助推江西文旅产业转型升级 10月19日,江西省文化和旅游厅与腾讯科技(上海)有限公司签订战略合作协议。双方将聚焦江西...

    腾讯文旅
  • 为WordPress加入Fancybox相册功能免插件实现

    可以在js里面建个相应文件夹放置,也可分开放置,若图片与其他文件分开记得修改css里面的图片链接地址。

    汐楓

扫码关注云+社区

领取腾讯云代金券