「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇
一、技术点
1.1 技术点
在之前的方法中有一个参数:
- public static void premain(String agentArgs, Instrumentation inst) {
- System.out.println("Hello javaagent permain:"+agentArgs);
- }
参数Instrumentation是一个接口,我们可以看下:
- public interface Instrumentation {
- /**
- 注册一个Transformer,从此之后的类加载都会被Transformer拦截。
- * Transformer可以直接对类的字节码byte[]进行修改
-
- * @since 1.6
- */
- void
- addTransformer(ClassFileTransformer transformer);
-
-
- /**
-
- * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
- * retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
- *
- * @see #isRetransformClassesSupported
- * @see #addTransformer
- * @see java.lang.instrument.ClassFileTransformer
- * @since 1.6
- */
- void
- retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
-
-
-
-
- /**
- 获取当前被JVM加载的所有类对象
- */
- @SuppressWarnings("rawtypes")
- Class[]
- getAllLoadedClasses();
-
-
- }
前面两个方法比较重要,addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。
1.2 javaassist
直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码。
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。
二、实现
在接下来我看下如何统计方法的耗时。
2.1 添加依赖
在pom.xml文件添加依赖:
- <dependencies>
- <dependency>
- <groupId>org.javassist</groupId>
- <artifactId>javassist</artifactId>
- <version>3.27.0-GA</version>
- </dependency>
2.2 实现自定义的ClassFileTransformer
实现自定义的ClassFileTransformer,代码如下
- package com.kfit.test4;
-
- import java.io.ByteArrayInputStream;
- import java.lang.instrument.ClassFileTransformer;
- import java.security.ProtectionDomain;
- import javassist.ClassPool;
- import javassist.CtClass;
- import javassist.CtMethod;
- public class TimeConsumingTransformer implements ClassFileTransformer {
-
- @Override
- public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
- ProtectionDomain protectionDomain, byte[] classfileBuffer) {
- // 这里我们限制下,只针对目标包下进行耗时统计
- if (!className.startsWith("com/kfit")) {
- return classfileBuffer;
- }
-
- CtClass cl = null;
- try {
- ClassPool classPool = ClassPool.getDefault();
- cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
-
- if(cl.isInterface()) {//接口的情况下,就不用处理了。
- //报异常:javassist.CannotCompileException: no method body
- //这个接口,类似public interface PermissionService {};
- return 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;
- }
- }
2.3 修改agent
对于agent稍微修改下:
- package com.kfit;
-
- import java.lang.instrument.Instrumentation;
- import com.kfit.test4.TimeConsumingTransformer;
-
- public class MyAgent2 {
- /**
- * jvm 参数形式启动,运行此方法
- *
- * manifest需要配置属性Premain-Class
- * @param agentArgs
- * @param inst
- */
- public static void premain(String agentArgs, Instrumentation inst) {
- System.out.println("premain");
- addTimeConsumingTransformer(inst);
- }
-
- /**
- * 动态 attach 方式启动,运行此方法
- *
- * manifest需要配置属性Agent-Class
- *
- * @param agentArgs
- * @param inst
- */
- public static void agentmain(String agentArgs, Instrumentation inst) {
- System.out.println("agentmain");
- addTimeConsumingTransformer(inst);
- }
-
- /**
- * 统计方法耗时
- * @param inst
- */
- private static void addTimeConsumingTransformer(Instrumentation inst) {
- System.out.println("addTimeConsumingTransformer");
- inst.addTransformer(new TimeConsumingTransformer(), true);
- }
- }
2.4 配置plugin
在pom.xml文件配置plugin:
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <version>2.2</version>
- <configuration>
- <archive>
- <manifestEntries>
- <Project-name>${project.name}</Project-name>
- <Project-version>${project.version}</Project-version>
- <Premain-Class>com.kfit.MyAgent2</Premain-Class>
- <Can-Redefine-Classes>true</Can-Redefine-Classes>
- <Can-Retransform-Classes>true</Can-Retransform-Classes>
- <Boot-Class-Path>javassist-3.27.0-GA.jar</Boot-Class-Path>
- </manifestEntries>
- </archive>
- <skip>true</skip>
- </configuration>
- </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