相关历史文章(阅读本文之前,您可能需要先看下之前的系列)
国内最全的Spring Boot系列之三
Java语言的优雅停机 - 第308篇
SpringBoot 优雅停止服务的几种方法 - 第309篇
Docker优雅的关闭SpringBoot - 第310篇
「字节码插桩」统计方法耗时(第一篇:初出茅庐)- 第311篇
「字节码插桩」统计方法耗时(第二篇:崭露头角)- 第311篇
师傅:上一节,我们对于javaagent有了一个简单的认知,但是对于如何修改修改字节码,我们还没进行讲解,这节,我们一起来学习下。
徒儿:实在是太棒了,终于到了重点了,等的花都谢了。
师傅:没听说过好事多谋嘛。
徒儿:师傅,你都要把我的心魔化了。
师傅:你看你连这么一丁点的耐性都没有,以后可怎么追女孩子呢。
徒儿:师傅,搞得你已经有女朋友了似的。
师傅:你这不怼为师,你会死么?
徒儿:师傅,我错了,我口快心非,咱们还是赶紧进入今天的正题吧。
一、技术点
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文件添加依赖:
org.javassist
javassist
3.27.0-GA
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:
org.apache.maven.plugins
maven-jar-plugin
2.2
${project.name}
${project.version}
com.kfit.MyAgent2
true
true
javassist-3.27.0-GA.jar
true
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中的内容。
我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。
学院中有Spring Boot相关的课程:
à悟空学院:https://t.cn/Rg3fKJD
SpringBoot视频:http://t.cn/A6ZagYTi
Spring Cloud视频:http://t.cn/A6ZagxSR
SpringBoot Shiro视频:http://t.cn/A6Zag7IV
SpringBoot交流平台:https://t.cn/R3QDhU0
SpringData和JPA视频:http://t.cn/A6Zad1OH
SpringSecurity5.0视频:http://t.cn/A6ZadMBe
Sharding-JDBC分库分表实战:http://t.cn/A6ZarrqS
分布式事务解决方案「手写代码」:http://t.cn/A6ZaBnIr