使用javassist编写一个简单的agentClassTransformer

前段时间找到一个很好的工具Greys-anatomy, 相应的参考地址为参考地址:https://github.com/oldmanpushcart/greys-anatomy. 为此,专门研究了一下基工作原理.并简单研究了一下基于Instrumentation进行运行时代码调整的一些实现手法. 本文, 简单介绍一下如何使用javassist来简单对一个代码作编织, 实现简单的一些监控指标手段.

附: 另外,前几年淘宝也简单作了一个通过启动时agent达到运行方法监控的目的, 称之为TProfiler,对研究一些实现手法很有用.

一个主要的ClassFileTransformer定义如下:

    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

传递到当前实现的主要有用的信息即为className, classfileBuffer两个数据. className即为当前正准备加载的类, 而classfileBuffer即传递给当前处理的字节码, 需要做的即是将这个字节码进行处理,然后返回一个已经处理过的字节码,即完成instrument操作.

限制要进行处理的类

因为本身并不是所有的类都需要进行处理,因此在这里必须要传递到这里的数据过一个简单的过滤,一个简单的判断,就是作类名处理. 如下参考所示:

        //非自有类, 直接返回
        if(!className.startsWith("com/iflym") && !className.startsWith("io/iflym")) {
            return classfileBuffer;
        }
        //为null,表示由bootstrapLoader加载的,不能重写
        if(loader == null) {
            return classfileBuffer;
        }

当然,这里可以作一个复杂的过滤,比如通过正则表达式来处理, 最终实际上还是定位到要处理类.

类转换, 添加方法执行监控处理

因为第一步已经拿到相应的字节码,那么第二步就是分析这个字节码, 读取为一个简单的类结构, 遍列所有的方法列表,然后在方法执行的主要部分添加我们要处理的逻辑即可. 在整个过程中, 不能将其直接实例化, 因为一旦实例化, 即不可再进行修改. 强制采用class.forName或classLoader.defineClass会产生如下的错误.

    java.lang.LinkageError: loader (instance of  XXX): attempted
        duplicate class definition for name: "XXX"

这里采用一个额外的字节码框架来完成读取操作并处理, javassist.

因为在整个过程中, 需要处理的信息太多, 因此必须使用额外的数据结构来保存在执行过程中的信息, 并且使用javassist作代码修改, local变量在某些情况下并不能满足要求(如在finally中新添加的local变量不能够被访问到)

一个如下的Around实现参考:

        //使用一个有作用域的类池来进行类数据处理
        ScopedClassPool classPool = ScopedClassPoolRepositoryImpl.getInstance().createScopedClassPool(loader, ClassPool.getDefault());
        classPool.appendClassPath(new ByteArrayClassPath(fullName, bytes));

        CtClass ctClass = classPool.getLocally(fullName);

        if(ctClass.isFrozen()) {
            return ctClass.toBytecode();
        }

        CtMethod[] ctMethods = ctClass.getDeclaredMethods();
        for(CtMethod method : ctMethods) {
            //实际代码执行前, 记录时间信息 这里使用一个线程本地的栈记录时间
            method.insertBefore("{com.iflym.LongHolder holder = com.iflym.LongHolder.start();" +
                    "holder.start = System.currentTimeMillis();}");
            //在finally中获取刚才的栈记录,然后简单地作一个时间记录处理
            method.insertAfter("{com.iflym.LongHolder holder = com.iflym.LongHolder.end();" +
                    "holder.end = System.currentTimeMillis();" +
                    "long value = holder.end - holder.start;" +
                    "" +
                    " System.out.println(\"执行方法:\" + \"" + method.getName() + "\" + \"执行花费\" + (value));}", false);
        }

        //每个方法都调整了, 那么现在可以直接返回此已经处理过的字节
        byte[] value = ctClass.toBytecode();
        classPool.flushClass(fullName);
        classPool.close();

        return value;

采用类似的手法, 还可以增加比如时间监控, 参数监控, 甚至于修改数据这样的功能. 

在当前的开源框架中, 已经有很多的组件提供类似的功能,比如aspectj,已经内置一整套实现方法. 当然这些组件实现的手法都有自己限制的应用场景. 在理解了具体的实现手法之后, 接下来就是如何将此代码编织应用到项目当中.

至于像grey这个组件, 它并不要求自己的系统在运行前引入agent变量, 而是通过java tools 中 attach组件远程注入agent来达到目的. 当然这整个环节还是比较有意义的.

在编写此篇代码时, 还是出了很多问题, 比如如何多次加载单个字节码文件, 或者如何织入需要处理的代码等, 在不涉及到字节码层面, 在语义层面作这些事情,里面有坊也比较多,需要多一些细心.

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201711090001.html

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

邮箱地址不会被公开。 必填项已用*标注