前段时间找到一个很好的工具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