前面有两篇铺垫博文,在博文《200303-如何优雅的在 java 中统计代码块耗时》,其最后提到了根据利用 java agent 来统计方法耗时git
博文《200316-IDEA + maven 零基础构建 java agent 项目》中则详细描述了搭建一个 java agent 开发测试项目的全过程github
本篇博文将进入 java agent 的实战,手把手教你如何是实现一个统计方法耗时的 java agentbootstrap
<!-- more -->app
上面两节虽然手把手教你实现了一个 hello world 版 agent,然而实际上对 java agent 依然是一脸茫然,因此咱们得先补齐一下基础知识jvm
首先来看 agent 的两个方法中的参数 Instrumentation
,咱们先看一下它的接口定义maven
/** * 注册一个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,不然不会恢复。ide
经过上面的描述,可知学习
Transformer
修改类咱们须要统计方法耗时,因此想到的就是在方法的执行前,记录一个时间,执行完以后统计一下时间差,即为耗时测试
直接修改字节码有点麻烦,所以咱们借助神器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 过程当中遇到的一个问题
在演示方法耗时的 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 开发环境下,以下配置便可
再次运行,正常了
本篇为实战项目,首先明确方法参数Instrumentation
它的接口定义,经过它来实现 java 字节码的修改
咱们经过实现自定义的ClassFileTransformer
,借助 javassist 来修改字节码,为每一个方法的第一行和最后一行注入耗时统计的代码,从而实现方法耗时统计
最后留一个小问题,上面的实现中,当方法内部抛出异常时,咱们注入的最后一行统计耗时会不会如期输出,若是不会,应该怎么修改,欢迎各位大佬留言指出解决方案
(具体解决方案能够在源码中获取哦,还有配套的测试 case,求支持,求赞,求关注 ❀)
相关博文
相关源码
一灰灰的我的博客,记录全部学习和工做中的博文,欢迎你们前去逛逛
尽信书则不如,已上内容,纯属一家之言,因我的能力有限,不免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
一灰灰 blog