作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、来自深夜的电话!
咋滴,你那上线的系统是裸奔呢?
周末熟睡的深夜,突然接到老板电话☎的催促。“赶紧看微信、看微信,咋系统出问题了,我们都不知道,还得用户反馈才知道的!!!”深夜爬起来,打开电脑连上 VPN ,打着哈欠、睁开朦胧的眼睛,查查系统日志,原来是系统挂了,赶紧重启恢复!
虽然重启恢复了系统,也重置了老板扭曲的表情。但系统是怎么挂的呢,因为没有一个监控系统,也不知道是流量太大导致,还是因为程序问题引起,通过一片片的日志,也仅能粗略估计出一些打着好像的标签
给老板汇报。不过老板也不傻,聊来聊去,让把所有的系统运行状况都监控出来。
双手拖着困倦的脑袋,一时半会也想不出什么好方法,难道在每个方法上都硬编码上执行耗时计算。之后把信息在统一收集起来,展示到一个监控页面呢,监控页面使用阿帕奇的 echarts,别说要是这样显示了,还真能挺好看还好用。
其实一套线上系统是否稳定运行,取决于它的运行健康度,而这包括;调用量、可用率、影响时长以及服务器性能等各项指标的一个综合值。并且在系统出现异常问题时,可以抓取整个业务方法执行链路并输出;当时的入参、出参、异常信息等等。当然还包括一些 JVM、Redis、Mysql 的各项性能指标,以用于快速定位并解决问题。
那么要做到这样的事情有什么处理方案呢,其实做法还是比较多的,比如;
最简单粗暴的就是硬编码在方法中,收取执行耗时以及出入参和异常信息。但这样的编码成本实在太大,而且硬编码完还需要大量回归测试,可能给系统带来一定的风险。万一谁手抖给复制粘贴错了呢!
可以选择切面方式做一套统一监控的组件,相对来说还是好一些的。但也需要硬编码,比如写入注解,同时维护成本也不低。
其实市面上对于这样的监控其实是有整套的非入侵监控方案的,比如;Google Dapper、Zipkin 等都可以实现监控系统需求,他们都是基于探针技术非入侵的采用字节码增强的方式采集系统运行信息进行分析和监控运行状态。
好,那么本文就来带着大家来尝试下几种不同方式,监控系统运行状态的实现思路。
二、准备工作
本文会基于 AOP
、字节码框架(ASM
、Javassist
、Byte-Buddy
),分别实现不同的监控实现代码。整个工程结构如下:
MonitorDesign
├── cn-bugstack-middleware-aop
├── cn-bugstack-middleware-asm
├── cn-bugstack-middleware-bytebuddy
├── cn-bugstack-middleware-javassist
├── cn-bugstack-middleware-test
└── pom.xml
复制代码
cn-bugstack-middleware-test
@RestController
public class UserController {
private Logger logger = LoggerFactory.getLogger(UserController.class);
/**
* 测试:http://localhost:8081/api/queryUserInfo?userId=aaa
*/
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
logger.info("查询用户信息,userId:{}", userId);
return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
}
}
复制代码
三、使用 AOP 做个切面监控
1. 工程结构
cn-bugstack-middleware-aop
└── src
├── main
│ └── java
│ ├── cn.bugstack.middleware.monitor
│ │ ├── annotation
│ │ │ └── DoMonitor.java
│ │ ├── config
│ │ │ └── MonitorAutoConfigure.java
│ │ └── DoJoinPoint.java
│ └── resources
│ └── META-INF
│ └── spring.factories
└── test
└── java
└── cn.bugstack.middleware.monitor.test
└── ApiTest.java
复制代码
基于 AOP 实现的监控系统,核心逻辑的以上工程并不复杂,其核心点在于对切面的理解和运用,以及一些配置项需要按照 SpringBoot 中的实现方式进行开发。
DoMonitor,是一个自定义注解。它作用就是在需要使用到的方法监控接口上,添加此注解并配置必要的信息。
MonitorAutoConfigure,配置下是可以对 SpringBoot yml 文件的使用,可以处理一些 Bean 的初始化操作。
DoJoinPoint,是整个中间件的核心部分,它负责对所有添加自定义注解的方法进行拦截和逻辑处理。
2. 定义监控注解
cn.bugstack.middleware.monitor.annotation.DoMonitor
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {
String key() default "";
String desc() default "";
}
复制代码
@Retention(RetentionPolicy.RUNTIME),Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
@Retention 是注解的注解,也称作元注解。这个注解里面有一个入参信息 RetentionPolicy.RUNTIME
在它的注释中有这样一段描述:Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
其实说的就是加了这个注解,它的信息会被带到 JVM 运行时,当你在调用方法时可以通过反射拿到注解信息。除此之外,RetentionPolicy 还有两个属性 SOURCE
、CLASS
,其实这三个枚举正式对应了 Java 代码的加载和运行顺序,Java 源码文件 -> .class 文件 -> 内存字节码。并且后者范围大于前者,所以一般情况下只需要使用 RetentionPolicy.RUNTIME 即可。
@Target 也是元注解起到标记作用,它的注解名称就是它的含义,目标,也就是我们这个自定义注解 DoWhiteList 要放在类、接口还是方法上。在 JDK1.8 中 ElementType 一共提供了 10 中目标枚举,TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE,可以参考自己的自定义注解作用域进行设置
自定义注解 @DoMonitor 提供了监控的 key 和 desc 描述,这个主要记录你监控方法的为唯一值配置和对监控方法的文字描述。
3. 定义切面拦截
cn.bugstack.middleware.monitor.DoJoinPoint
@Aspect
public class DoJoinPoint {
@Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
public void aopPoint() {
}
@Around("aopPoint() && @annotation(doMonitor)")
public Object doRouter(ProceedingJoinPoint jp, DoMonitor doMonitor) throws Throwable {
long start = System.currentTimeMillis();
Method method = getMethod(jp);
try {
return jp.proceed();
} finally {
System.out.println("监控 - Begin By AOP");
System.out.println("监控索引:" + doMonitor.key());
System.out.println("监控描述:" + doMonitor.desc());
System.out.println("方法名称:" + method.getName());
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("监控 - End\r\n");
}
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
}
复制代码
使用注解 @Aspect,定义切面类。这是一个非常常用的切面定义方式。
@Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
,定义切点。在 Pointcut 中提供了很多的切点寻找方式,有指定方法名称的、有范围筛选表达式的,也有我们现在通过自定义注解方式的。一般在中间件开发中,自定义注解方式使用的比较多,因为它可以更加灵活的运用到各个业务系统中。
@Around("aopPoint() && @annotation(doMonitor)")
,可以理解为是对方法增强的织入动作,有了这个注解的效果就是在你调用已经加了自定义注解 @DoMonitor 的方法时,会先进入到此切点增强的方法。那么这个时候就你可以做一些对方法的操作动作了,比如我们要做一些方法监控和日志打印等。
最后在 doRouter
方法体中获取把方法执行 jp.proceed();
使用 try finally
包装起来,并打印相关的监控信息。这些监控信息的获取最后都是可以通过异步消息的方式发送给服务端,再由服务器进行处理监控数据和处理展示到监控页面。
4. 初始化切面类
cn.bugstack.middleware.monitor.config.MonitorAutoConfigure
@Configuration
public class MonitorAutoConfigure {
@Bean
@ConditionalOnMissingBean
public DoJoinPoint point(){
return new DoJoinPoint();
}
}
复制代码
5. 运行测试
5.1 引入 POM 配置
<!-- 监控方式:AOP -->
<dependency>
<groupId>cn.bugstack.middleware</groupId>
<artifactId>cn-bugstack-middleware-aop</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
复制代码
5.2 方法上配置监控注册
@DoMonitor(key = "cn.bugstack.middleware.UserController.queryUserInfo", desc = "查询用户信息")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
logger.info("查询用户信息,userId:{}", userId);
return new UserInfo("虫虫:" + userId, 19, "天津市东丽区万科赏溪苑14-0000");
}
复制代码
5.3 测试结果
2021-07-04 23:21:10.710 INFO 19376 --- [nio-8081-exec-1] c.b.m.test.interfaces.UserController : 查询用户信息,userId:aaa
监控 - Begin By AOP
监控索引:cn.bugstack.middleware.UserController.queryUserInfo
监控描述:查询用户信息
方法名称:queryUserInfo
方法耗时:6ms
监控 - End
复制代码
接下来我们开始介绍关于使用字节码插桩非入侵的方式进行系统监控,关于字节码插桩常用的有三个组件,包括:ASM、Javassit、Byte-Buddy,接下来我们分别介绍它们是如何使用的。
四、ASM
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
1. 先来个测试
cn.bugstack.middleware.monitor.test.ApiTest
private static byte[] generate() {
ClassWriter classWriter = new ClassWriter(0);
// 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "cn/bugstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
// 添加方法;修饰符、方法名、描述符、签名、异常
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
// 执行指令;获取静态属性
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载常量 load constant
methodVisitor.visitLdcInsn("Hello World ASM!");
// 调用方法
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 返回
methodVisitor.visitInsn(Opcodes.RETURN);
// 设置操作数栈的深度和局部变量的大小
methodVisitor.visitMaxs(2, 1);
// 方法结束
methodVisitor.visitEnd();
// 类完成
classWriter.visitEnd();
// 生成字节数组
return classWriter.toByteArray();
}
复制代码
以上这段代码就是基于 ASM 编写的 HelloWorld,整个过程包括:定义一个类的生成 ClassWriter、设定版本、修饰符、全类名、签名、父类、实现的接口,其实也就是那句;public class HelloWorld
类型描述符:
方法描述符:
执行指令;获取静态属性。主要是获得 System.out
加载常量 load constant,输出我们的 HelloWorld methodVisitor.visitLdcInsn("Hello World");
最后是调用输出方法并设置空返回,同时在结尾要设置操作数栈的深度和局部变量的大小。
这样输出一个 HelloWorld
是不还是蛮有意思的,虽然你可能觉得这编码起来实在太难了吧,也非常难理解。不过你可以安装一个 ASM 在 IDEA 中的插件 ASM Bytecode Outline,能更加方便的查看一个普通的代码在使用 ASM 的方式该如何处理。
另外以上这段代码的测试结果,主要是生成一个 class 文件和输出 Hello World ASM!
结果。
2. 监控设计工程结构
cn-bugstack-middleware-asm
└── src
├── main
│ ├── java
│ │ └── cn.bugstack.middleware.monitor
│ │ ├── config
│ │ │ ├── MethodInfo.java
│ │ │ └── ProfilingFilter.java
│ │ ├── probe
│ │ │ ├── ProfilingAspect.java
│ │ │ ├── ProfilingClassAdapter.java
│ │ │ ├── ProfilingMethodVisitor.java
│ │ │ └── ProfilingTransformer.java
│ │ └── PreMain.java
│ └── resources
│ └── META_INF
│ └── MANIFEST.MF
└── test
└── java
└── cn.bugstack.middleware.monitor.test
└── ApiTest.java
复制代码
以上工程结构是使用 ASM 框架给系统方法做增强操作,也就是相当于通过框架完成硬编码写入方法前后的监控信息。不过这个过程转移到了 Java 程序启动时在 Javaagent#premain 进行处理。
MethodInfo 是方法的定义,主要是描述类名、方法名、描述、入参、出参信息。
ProfilingFilter 是监控的配置信息,主要是过滤一些不需要字节码增强操作的方法,比如 main、hashCode、javax/等
ProfilingAspect、ProfilingClassAdapter、ProfilingMethodVisitor、ProfilingTransformer,这四个类主要是完成字节码插装操作和输出监控结果的类。
PreMain 提供了 Javaagent 的入口,JVM 首先尝试在代理类上调用 premain 方法。
MANIFEST.MF 是配置信息,主要是找到 Premain-Class Premain-Class: cn.bugstack.middleware.monitor.PreMain
3. 监控类入口
cn.bugstack.middleware.monitor.PreMain
public class PreMain {
//JVM 首先尝试在代理类上调用以下方法
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ProfilingTransformer());
}
//如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
public static void premain(String agentArgs) {
}
}
复制代码
4. 字节码方法处理
cn.bugstack.middleware.monitor.probe.ProfilingTransformer
public class ProfilingTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
if (ProfilingFilter.isNotNeedInject(className)) {
return classfileBuffer;
}
return getBytes(loader, className, classfileBuffer);
} catch (Throwable e) {
System.out.println(e.getMessage());
}
return classfileBuffer;
}
private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ProfilingClassAdapter(cw, className);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
}
复制代码
5.字节码方法解析
cn.bugstack.middleware.monitor.probe.ProfilingMethodVisitor
public class ProfilingMethodVisitor extends AdviceAdapter {
private List<String> parameterTypeList = new ArrayList<>();
private int parameterTypeCount = 0; // 参数个数
private int startTimeIdentifier; // 启动时间标记
private int parameterIdentifier; // 入参内容标记
private int methodId = -1; // 方法全局唯一标记
private int currentLocal = 0; // 当前局部变量值
private final boolean isStaticMethod; // true;静态方法,false;非静态方法
private final String className;
protected ProfilingMethodVisitor(int access, String methodName, String desc, MethodVisitor mv, String className, String fullClassName, String simpleClassName) {
super(ASM5, mv, access, methodName, desc);
this.className = className;
// 判断是否为静态方法,非静态方法中局部变量第一个值是this,静态方法是第一个入参参数
isStaticMethod = 0 != (access & ACC_STATIC);
//(String var1,Object var2,String var3,int var4,long var5,int[] var6,Object[][] var7,Req var8)=="(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)V"
Matcher matcher = Pattern.compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})").matcher(desc.substring(0, desc.lastIndexOf(')') + 1));
while (matcher.find()) {
parameterTypeList.add(matcher.group(1));
}
parameterTypeCount = parameterTypeList.size();
methodId = ProfilingAspect.generateMethodId(new MethodInfo(fullClassName, simpleClassName, methodName, desc, parameterTypeList, desc.substring(desc.lastIndexOf(')') + 1)));
}
//... 一些字节码插桩操作
}
复制代码
当程序启动加载的时候,每个类的每一个方法都会被监控到。类的名称、方法的名称、方法入参出参的描述等,都可以在这里获取。
为了可以在后续监控处理不至于每一次都去传参(方法信息)浪费消耗性能,一般这里都会给每个方法生产一个全局防重的 id
,通过这个 id
就可以查询到对应的方法。
另外从这里可以看到的方法的入参和出参被描述成一段指定的码,(II)Ljava/lang/String;
,为了我们后续对参数进行解析,那么需要将这段字符串进行拆解。
6. 运行测试
6.1 配置 VM 参数 Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-asm\target\cn-bugstack-middleware-asm.jar
复制代码
6.2 测试结果
监控 - Begin By ASM
方法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:54(s)
监控 - End
复制代码
五、Javassist
Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码 JBoss 应用服务器项目,通过使用 Javassist 对字节码操作为 JBoss 实现动态"AOP"框架。
1. 先来个测试
cn.bugstack.middleware.monitor.test.ApiTest
public class ApiTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("cn.bugstack.middleware.javassist.MathUtil");
// 属性字段
CtField ctField = new CtField(CtClass.doubleType, "π", ctClass);
ctField.setModifiers(Modifier.PRIVATE + Modifier.STATIC + Modifier.FINAL);
ctClass.addField(ctField, "3.14");
// 方法:求圆面积
CtMethod calculateCircularArea = new CtMethod(CtClass.doubleType, "calculateCircularArea", new CtClass[]{CtClass.doubleType}, ctClass);
calculateCircularArea.setModifiers(Modifier.PUBLIC);
calculateCircularArea.setBody("{return π * $1 * $1;}");
ctClass.addMethod(calculateCircularArea);
// 方法;两数之和
CtMethod sumOfTwoNumbers = new CtMethod(pool.get(Double.class.getName()), "sumOfTwoNumbers", new CtClass[]{CtClass.doubleType, CtClass.doubleType}, ctClass);
sumOfTwoNumbers.setModifiers(Modifier.PUBLIC);
sumOfTwoNumbers.setBody("{return Double.valueOf($1 + $2);}");
ctClass.addMethod(sumOfTwoNumbers);
// 输出类的内容
ctClass.writeFile();
// 测试调用
Class clazz = ctClass.toClass();
Object obj = clazz.newInstance();
Method method_calculateCircularArea = clazz.getDeclaredMethod("calculateCircularArea", double.class);
Object obj_01 = method_calculateCircularArea.invoke(obj, 1.23);
System.out.println("圆面积:" + obj_01);
Method method_sumOfTwoNumbers = clazz.getDeclaredMethod("sumOfTwoNumbers", double.class, double.class);
Object obj_02 = method_sumOfTwoNumbers.invoke(obj, 1, 2);
System.out.println("两数和:" + obj_02);
}
}
复制代码
生成的类
public class MathUtil {
private static final double π = 3.14D;
public double calculateCircularArea(double var1) {
return 3.14D * var1 * var1;
}
public Double sumOfTwoNumbers(double var1, double var3) {
return var1 + var3;
}
public MathUtil() {
}
}
复制代码
测试结果
圆面积:4.750506
两数和:3.0
Process finished with exit code 0
复制代码
2. 监控设计工程结构
cn-bugstack-middleware-javassist
└── src
├── main
│ ├── java
│ │ └── cn.bugstack.middleware.monitor
│ │ ├── config
│ │ │ └── MethodDescription.java
│ │ ├── probe
│ │ │ ├── Monitor.java
│ │ │ └── MyMonitorTransformer.java
│ │ └── PreMain.java
│ └── resources
│ └── META_INF
│ └── MANIFEST.MF
└── test
└── java
└── cn.bugstack.middleware.monitor.test
└── ApiTest.java
复制代码
3. 监控方法插桩
cn.bugstack.middleware.monitor.probe.MyMonitorTransformer
public class MyMonitorTransformer implements ClassFileTransformer {
private static final Set<String> classNameSet = new HashSet<>();
static {
classNameSet.add("cn.bugstack.middleware.test.interfaces.UserController");
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
try {
String currentClassName = className.replaceAll("/", ".");
if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
return null;
}
// 获取类
CtClass ctClass = ClassPool.getDefault().get(currentClassName);
String clazzName = ctClass.getName();
// 获取方法
CtMethod ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
String methodName = ctMethod.getName();
// 方法信息:methodInfo.getDescriptor();
MethodInfo methodInfo = ctMethod.getMethodInfo();
// 方法:入参信息
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
CtClass[] parameterTypes = ctMethod.getParameterTypes();
boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0; // 判断是否为静态方法
int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
List<String> parameterNameList = new ArrayList<>(parameterSize); // 入参名称
List<String> parameterTypeList = new ArrayList<>(parameterSize); // 入参类型
StringBuilder parameters = new StringBuilder(); // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化
for (int i = 0; i < parameterSize; i++) {
parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
parameterTypeList.add(parameterTypes[i].getName());
if (i + 1 == parameterSize) {
parameters.append("$").append(i + 1);
} else {
parameters.append("$").append(i + 1).append(",");
}
}
// 方法:出参信息
CtClass returnType = ctMethod.getReturnType();
String returnTypeName = returnType.getName();
// 方法:生成方法唯一标识ID
int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
// 定义属性
ctMethod.addLocalVariable("startNanos", CtClass.longType);
ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
// 方法前加强
ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");
// 方法后加强
ctMethod.insertAfter("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
// 方法;添加TryCatch
ctMethod.addCatch("{ cn.bugstack.middleware.monitor.probe.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception")); // 添加异常捕获
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
复制代码
与 ASM 实现相比,整体的监控方法都是类似的,所以这里只展示下不同的地方。
通过 Javassist 的操作,主要是实现一个 ClassFileTransformer
接口的 transform 方法,在这个方法中获取字节码并进行相应的处理。
处理过程包括:获取类、获取方法、获取入参信息、获取出参信息、给方法生成唯一 ID、之后开始进行方法的前后增强操作,这个增强也就是在方法块中添加监控代码。
最后返回字节码信息 return ctClass.toBytecode();
现在你新加入的字节码就已经可以被程序加载处理了。
4. 运行测试
4.1 配置 VM 参数 Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-javassist\target\cn-bugstack-middleware-javassist.jar
复制代码
4.2 测试结果
监控 - Begin By Javassist
方法:cn.bugstack.middleware.test.interfaces.UserController$$EnhancerBySpringCGLIB$$8f5a18ca.queryUserInfo
入参:null 入参类型:["Ljava/lang/String;"] 入数[值]:["aaa"]
出参:Lcn/bugstack/middleware/test/interfaces/dto/UserInfo; 出参[值]:{"address":"天津市东丽区万科赏溪苑14-0000","age":19,"code":"0000","info":"success","name":"虫虫:aaa"}
耗时:46(s)
监控 - End
复制代码
六、Byte-Buddy
2015 年 10 月,Byte Buddy 被 Oracle 授予了 Duke's Choice 大奖。该奖项对 Byte Buddy 的“ Java 技术方面的巨大创新 ”表示赞赏。我们为获得此奖项感到非常荣幸,并感谢所有帮助 Byte Buddy 取得成功的用户以及其他所有人。我们真的很感激!
Byte Buddy
是一个代码生成和操作库,用于在 Java
应用程序运行时创建和修改 Java
类,而无需编译器的帮助。除了 Java
类库附带的代码生成实用程序外,Byte Buddy
还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy
提供了一种方便的 API,可以使用 Java
代理或在构建过程中手动更改类。
无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。
已支持 Java 11,库轻量,仅取决于 Java 字节代码解析器库 ASM 的访问者 API,它本身不需要任何其他依赖项。
比起 JDK 动态代理、cglib、Javassist,Byte Buddy 在性能上具有一定的优势。
1. 先来个测试
cn.bugstack.middleware.monitor.test.ApiTest
public class ApiTest {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
String helloWorld = new ByteBuddy()
.subclass(Object.class)
.method(named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(ApiTest.class.getClassLoader())
.getLoaded()
.newInstance()
.toString();
System.out.println(helloWorld);
}
}
复制代码
测试结果
Hello World!
Process finished with exit code 0
复制代码
2. 监控设计工程结构
cn-bugstack-middleware-bytebuddy
└── src
├── main
│ ├── java
│ │ └── cn.bugstack.middleware.monitor
│ │ ├── MonitorMethod
│ │ └── PreMain.java
│ └── resources
│ └── META_INF
│ └── MANIFEST.MF
└── test
└── java
└── cn.bugstack.middleware.monitor.test
└── ApiTest.java
复制代码
3. 监控方法插桩
cn.bugstack.middleware.monitor.MonitorMethod
public class MonitorMethod {
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception {
long start = System.currentTimeMillis();
Object resObj = null;
try {
resObj = callable.call();
return resObj;
} finally {
System.out.println("监控 - Begin By Byte-buddy");
System.out.println("方法名称:" + method.getName());
System.out.println("入参个数:" + method.getParameterCount());
for (int i = 0; i < method.getParameterCount(); i++) {
System.out.println("入参 Idx:" + (i + 1) + " 类型:" + method.getParameterTypes()[i].getTypeName() + " 内容:" + args[i]);
}
System.out.println("出参类型:" + method.getReturnType().getName());
System.out.println("出参结果:" + resObj);
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("监控 - End\r\n");
}
}
}
复制代码
常用注解说明
除了以上为了获取方法的执行信息使用到的注解外,Byte Buddy 还提供了很多其他的注解。如下;
常用核心 API
ByteBuddy
流式 API 方式的入口类
提供 Subclassing/Redefining/Rebasing 方式改写字节码
所有的操作依赖 DynamicType.Builder 进行,创建不可变的对象
ElementMatchers(ElementMatcher)
提供一系列的元素匹配的工具类(named/any/nameEndsWith 等等)
ElementMatcher(提供对类型、方法、字段、注解进行 matches 的方式,类似于 Predicate)
Junction 对多个 ElementMatcher 进行了 and/or 操作
DynamicType
(动态类型,所有字节码操作的开始,非常值得关注)
Unloaded(动态创建的字节码还未加载进入到虚拟机,需要类加载器进行加载)
Loaded(已加载到 jvm 中后,解析出 Class 表示)
Default(DynamicType 的默认实现,完成相关实际操作)
`Implementation
(用于提供动态方法的实现)
FixedValue(方法调用返回固定值)
MethodDelegation(方法调用委托,支持两种方式: Class 的 static 方法调用、object 的 instance method 方法调用)
Builder
(用于创建 DynamicType,相关接口以及实现后续待详解)
MethodDefinition
FieldDefinition
AbstractBase
4. 配置入口方法
cn.bugstack.middleware.monitor.PreMain
public class PreMain {
//JVM 首先尝试在代理类上调用以下方法
public static void premain(String agentArgs, Instrumentation inst) {
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
return builder
.method(ElementMatchers.named("queryUserInfo")) // 拦截任意方法
.intercept(MethodDelegation.to(MonitorMethod.class)); // 委托
};
new AgentBuilder
.Default()
.type(ElementMatchers.nameStartsWith(agentArgs)) // 指定需要拦截的类 "cn.bugstack.demo.test"
.transform(transformer)
.installOn(inst);
}
//如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
public static void premain(String agentArgs) {
}
}
复制代码
5. 运行测试
5.1 配置 VM 参数 Javaagent
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-bytebuddy\target\cn-bugstack-middleware-bytebuddy.jar
复制代码
5.2 测试结果
监控 - Begin By Byte-buddy
方法名称:queryUserInfo
入参个数:1
入参 Idx:1 类型:java.lang.String 内容:aaa
出参类型:cn.bugstack.middleware.test.interfaces.dto.UserInfo
出参结果:cn.bugstack.middleware.test.interfaces.dto.@214b199c
方法耗时:1ms
监控 - End
复制代码
七、总结
ASM 这种字节码编程的应用是非常广的,但可能确实平时看不到的,因为他都是与其他框架结合一起作为支撑服务使用。像这样的技术还有很多,比如 javassit、Cglib、jacoco 等等。
在一些全链路监控中的组件中 Javassist 的使用非常多,它即可使用编码的方式操作字节码增强,也可以像 ASM 那样进行处理。
Byte-buddy 是一个非常方便的框架,目前使用也越来越广泛,并且上手使用的学习难度也是几个框架中最低的。除了本章节的案例使用介绍外,还可以通过官网:https://bytebuddy.net
,去了解更多关于 Byte Buddy
的内容。
本章节所有的源码已经上传到 GitHub:https://github.com/fuzhengwei/MonitorDesign
八、系列推荐
评论