「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇

一、技术点

1.1 技术点

         在之前的方法中有一个参数:

  1. public static void premain(String agentArgs, Instrumentation inst) {
  2. System.out.println("Hello javaagent permain:"+agentArgs);
  3. }

         参数Instrumentation是一个接口,我们可以看下:

  1. public interface Instrumentation {
  2. /**
  3. 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
  4. * Transformer可以直接对类的字节码byte[]进行修改
  5. * @since 1.6
  6. */
  7. void
  8. addTransformer(ClassFileTransformer transformer);
  9. /**
  10. * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
  11. * retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
  12. *
  13. * @see #isRetransformClassesSupported
  14. * @see #addTransformer
  15. * @see java.lang.instrument.ClassFileTransformer
  16. * @since 1.6
  17. */
  18. void
  19. retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
  20. /**
  21. 获取当前被JVM加载的所有类对象
  22. */
  23. @SuppressWarnings("rawtypes")
  24. Class[]
  25. getAllLoadedClasses();
  26. }

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

 

1.2 javaassist

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

Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。

 

二、实现

         在接下来我看下如何统计方法的耗时。

 

2.1 添加依赖

         在pom.xml文件添加依赖:

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.javassist</groupId>
  4. <artifactId>javassist</artifactId>
  5. <version>3.27.0-GA</version>
  6. </dependency>

2.2 实现自定义的ClassFileTransformer

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

  1. package com.kfit.test4;
  2. import java.io.ByteArrayInputStream;
  3. import java.lang.instrument.ClassFileTransformer;
  4. import java.security.ProtectionDomain;
  5. import javassist.ClassPool;
  6. import javassist.CtClass;
  7. import javassist.CtMethod;
  8. public class TimeConsumingTransformer implements ClassFileTransformer {
  9. @Override
  10. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  11. ProtectionDomain protectionDomain, byte[] classfileBuffer) {
  12. // 这里我们限制下,只针对目标包下进行耗时统计
  13. if (!className.startsWith("com/kfit")) {
  14. return classfileBuffer;
  15. }
  16. CtClass cl = null;
  17. try {
  18. ClassPool classPool = ClassPool.getDefault();
  19. cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
  20. if(cl.isInterface()) {//接口的情况下,就不用处理了。
  21. //报异常:javassist.CannotCompileException: no method body
  22. //这个接口,类似public interface PermissionService {};
  23. return classfileBuffer;
  24. }
  25. for (CtMethod method : cl.getDeclaredMethods()) {
  26. // 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量
  27. method.addLocalVariable("start", CtClass.longType);
  28. method.insertBefore("start = System.currentTimeMillis();");
  29. String methodName = method.getLongName();
  30. method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" + ".currentTimeMillis() - start));");
  31. }
  32. byte[] transformed = cl.toBytecode();
  33. return transformed;
  34. } catch (Exception e) {
  35. e.printStackTrace();
  36. }
  37. return classfileBuffer;
  38. }
  39. }

2.3 修改agent

         对于agent稍微修改下:

  1. package com.kfit;
  2. import java.lang.instrument.Instrumentation;
  3. import com.kfit.test4.TimeConsumingTransformer;
  4. public class MyAgent2 {
  5. /**
  6. * jvm 参数形式启动,运行此方法
  7. *
  8. * manifest需要配置属性Premain-Class
  9. * @param agentArgs
  10. * @param inst
  11. */
  12. public static void premain(String agentArgs, Instrumentation inst) {
  13. System.out.println("premain");
  14. addTimeConsumingTransformer(inst);
  15. }
  16. /**
  17. * 动态 attach 方式启动,运行此方法
  18. *
  19. * manifest需要配置属性Agent-Class
  20. *
  21. * @param agentArgs
  22. * @param inst
  23. */
  24. public static void agentmain(String agentArgs, Instrumentation inst) {
  25. System.out.println("agentmain");
  26. addTimeConsumingTransformer(inst);
  27. }
  28. /**
  29. * 统计方法耗时
  30. * @param inst
  31. */
  32. private static void addTimeConsumingTransformer(Instrumentation inst) {
  33. System.out.println("addTimeConsumingTransformer");
  34. inst.addTransformer(new TimeConsumingTransformer(), true);
  35. }
  36. }

2.4 配置plugin

         在pom.xml文件配置plugin:

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-jar-plugin</artifactId>
  4. <version>2.2</version>
  5. <configuration>
  6. <archive>
  7. <manifestEntries>
  8. <Project-name>${project.name}</Project-name>
  9. <Project-version>${project.version}</Project-version>
  10. <Premain-Class>com.kfit.MyAgent2</Premain-Class>
  11. <Can-Redefine-Classes>true</Can-Redefine-Classes>
  12. <Can-Retransform-Classes>true</Can-Retransform-Classes>
  13. <Boot-Class-Path>javassist-3.27.0-GA.jar</Boot-Class-Path>
  14. </manifestEntries>
  15. </archive>
  16. <skip>true</skip>
  17. </configuration>
  18. </plugin>

 

2.5 打包

         使用clean package进行打包出来,为了不和之前冲突,取名为:

agentdemo-0.0.2-SNAPSHOT.jar

 

2.6 测试

         在Meimei所在的工程配置vm options:

-javaagent:/data/tmp/agentdemo-0.0.2-SNAPSHOT.jar=angel

         启动运行结果如下:



premain

addTimeConsumingTransformer

shopping:出发去和美眉一起逛街购物!

shopping:和美眉一起回家!

com.kfit.test.MeiMei.shopping() cost: 1005

com.kfit.test.MeiMei.sum(double,double) cost: 0

花了多少钱:5000.0

com.kfit.test.MeiMei.main(java.lang.String[]) cost: 1007

         看看这打印信息是不是很酷,我们都不需要修改源代码,可以说零侵入,就可以实现所有方法的时间的耗时统计,连main方法都打印出来时间了。

 

三、基于javaagent实现的框架

3.1全链路监控工具-Pinpoint

Pinpoint是一款全链路分析工具,提供了无侵入式的调用链监控、方法执行详情查看、应用状态信息监控等功能。基于GoogleDapper论文进行的实现,与另一款开源的全链路分析工具Zipkin类似,但相比Zipkin提供了无侵入式、代码维度的监控等更多的特性。 Pinpoint支持的功能比较丰富,感兴趣的可以自己去了解下。

 

3.2 Pinpoint实现原理

Pinpoint通过字节码增强技术(有的叫动态探针技术)来实现无侵入式的调用链采集。其核心实现原来还是基于JVM的javaagent机制来实现。

Pinpoint在启动时通过设置vm options:

-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar

         来指定pinpoint agent加载路径,在启动的时候agent将在加载应用class文件之前做拦截并修改字节码,在class方法调用的前后加上链路采集逻辑,从而实现链路采集功能。

         javaAgent的底层机制主要依赖JVMTI ,JVMTI全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。但JVMTI都是一些接口合集,需要有接口的实现,这就用到了java的instrument,可以理解instrument是JVMTI的一种实现,为JVM提供外挂支持。

 

悟纤小结

师傅:对于javaagent就介绍到这里,徒儿你把这两天的知识和大家总结下。

小结:

(1)javaagent:主要作用是在class 被加载之前对其拦截,以插入我们的监听字节码。

(2)javassist:修改字节码的工具类库。

(3)对于agent的使用方式就是在启动时候配置vm options: -javaagent: /agent.jar

(4)字节码插桩原理:在ClassLoader装载之前拦截修改class中的内容。

 


购买完整视频,请前往:http://www.mark-to-win.com/TeacherV2.html?id=287