咋滴,你那上线的系统是裸奔呢?
周末熟睡的深夜,突然接到老板电话☎的催促。“赶紧看微信、看微信,咋系统出问题了,我们都不知道,还得用户反馈才知道的!!!”深夜爬起来,打开电脑连上 VPN ,打着哈欠、睁开朦胧的眼睛,查查系统日志,原来是系统挂了,赶紧重启恢复!
虽然重启恢复了系统,也重置了老板扭曲的表情。但系统是怎么挂的呢,因为没有一个监控系统,也不知道是流量太大导致,还是因为程序问题引起,通过一片片的日志,也仅能粗略估计出一些打着好像的标签
给老板汇报。不过老板也不傻,聊来聊去,让把所有的系统运行状况都监控出来。
双手拖着困倦的脑袋,一时半会也想不出什么好方法,难道在每个方法上都硬编码上执行耗时计算。之后把信息在统一收集起来,展示到一个监控页面呢,监控页面使用阿帕奇的 echarts,别说要是这样显示了,还真能挺好看还好用。
其实一套线上系统是否稳定运行,取决于它的运行健康度,而这包括;调用量、可用率、影响时长以及服务器性能等各项指标的一个综合值。并且在系统出现异常问题时,可以抓取整个业务方法执行链路并输出;当时的入参、出参、异常信息等等。当然还包括一些JVM、Redis、Mysql的各项性能指标,以用于快速定位并解决问题。
那么要做到这样的事情有什么处理方案呢,其实做法还是比较多的,比如;
好,那么本文就来带着大家来尝试下几种不同方式,监控系统运行状态的实现思路。
本文会基于 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");
}
}
UserController#queryUserInfo
的方法执行信息为主,看看各类技术都是怎么操作的。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 中的实现方式进行开发。
cn.bugstack.middleware.monitor.annotation.DoMonitor
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMonitor {
String key() default "";
String desc() default "";
}
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.
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 即可。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());
}
}
@Pointcut("@annotation(cn.bugstack.middleware.monitor.annotation.DoMonitor)")
,定义切点。在 Pointcut 中提供了很多的切点寻找方式,有指定方法名称的、有范围筛选表达式的,也有我们现在通过自定义注解方式的。一般在中间件开发中,自定义注解方式使用的比较多,因为它可以更加灵活的运用到各个业务系统中。@Around("aopPoint() && @annotation(doMonitor)")
,可以理解为是对方法增强的织入动作,有了这个注解的效果就是在你调用已经加了自定义注解 @DoMonitor 的方法时,会先进入到此切点增强的方法。那么这个时候就你可以做一些对方法的操作动作了,比如我们要做一些方法监控和日志打印等。doRouter
方法体中获取把方法执行 jp.proceed();
使用 try finally
包装起来,并打印相关的监控信息。这些监控信息的获取最后都是可以通过异步消息的方式发送给服务端,再由服务器进行处理监控数据和处理展示到监控页面。cn.bugstack.middleware.monitor.config.MonitorAutoConfigure
@Configuration
public class MonitorAutoConfigure {
@Bean
@ConditionalOnMissingBean
public DoJoinPoint point(){
return new DoJoinPoint();
}
}
<!-- 监控方式:AOP -->
<dependency>
<groupId>cn.bugstack.middleware</groupId>
<artifactId>cn-bugstack-middleware-aop</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
@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");
}
2021-07-18 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
http://localhost:8081/api/queryUserInfo?userId=aaa
,可以看到已经可以把监控信息打印到控制台了。接下来我们开始介绍关于使用字节码插桩非入侵的方式进行系统监控,关于字节码插桩常用的有三个组件,包括:ASM、Javassit、Byte-Buddy,接下来我们分别介绍它们是如何使用的。
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
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();
}
public class HelloWorld
methodVisitor.visitLdcInsn("Hello World");
HelloWorld
是不还是蛮有意思的,虽然你可能觉得这编码起来实在太难了吧,也非常难理解。不过你可以安装一个 ASM 在 IDEA 中的插件 ASM Bytecode Outline,能更加方便的查看一个普通的代码在使用 ASM 的方式该如何处理。Hello World ASM!
结果。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 进行处理。
Premain-Class: cn.bugstack.middleware.monitor.PreMain
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) {
}
}
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();
}
}
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;
,为了我们后续对参数进行解析,那么需要将这段字符串进行拆解。-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-asm\target\cn-bugstack-middleware-asm.jar
VM options
中,jar包地址按照自己的路径进行配置。监控 - 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是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
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);
}
}
cn.bugstack.middleware.javassist.MathUtil
,同时还会在控制台输出结果。生成的类
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
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
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;
}
}
ClassFileTransformer
接口的 transform 方法,在这个方法中获取字节码并进行相应的处理。return ctClass.toBytecode();
现在你新加入的字节码就已经可以被程序加载处理了。-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-javassist\target\cn-bugstack-middleware-javassist.jar
VM options
中,jar包地址按照自己的路径进行配置。监控 - 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
2015年10月,Byte Buddy被 Oracle 授予了 Duke's Choice大奖。该奖项对Byte Buddy的“ Java技术方面的巨大创新 ”表示赞赏。我们为获得此奖项感到非常荣幸,并感谢所有帮助Byte Buddy取得成功的用户以及其他所有人。我们真的很感激!
Byte Buddy
是一个代码生成和操作库,用于在 Java
应用程序运行时创建和修改 Java
类,而无需编译器的帮助。除了 Java
类库附带的代码生成实用程序外,Byte Buddy
还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy
提供了一种方便的 API,可以使用 Java
代理或在构建过程中手动更改类。
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!
,整个代码块核心功能就是通过 method(named("toString"))
,找到 toString 方法,再通过拦截 intercept
,设定此方法的返回值。FixedValue.value("Hello World!")
。到这里其实一个基本的方法就通过 Byte-buddy
,最后加载、初始化和调用输出。测试结果
Hello World!
Process finished with exit code 0
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
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");
}
}
}
@Origin
,用于拦截原有方法,这样就可以获取到方法中的相关信息。常用注解说明
除了以上为了获取方法的执行信息使用到的注解外,Byte Buddy 还提供了很多其他的注解。如下;
注解 | 说明 |
---|---|
@Argument | 绑定单个参数 |
@AllArguments | 绑定所有参数的数组 |
@This | 当前被拦截的、动态生成的那个对象 |
@Super | 当前被拦截的、动态生成的那个对象的父类对象 |
@Origin | 可以绑定到以下类型的参数:Method 被调用的原始方法 Constructor 被调用的原始构造器 Class 当前动态创建的类 MethodHandle MethodType String 动态类的toString()的返回值 int 动态方法的修饰符 |
@DefaultCall | 调用默认方法而非super的方法 |
@SuperCall | 用于调用父类版本的方法 |
@Super | 注入父类型对象,可以是接口,从而调用它的任何方法 |
@RuntimeType | 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查 |
@Empty | 注入参数的类型的默认值 |
@StubValue | 注入一个存根值。对于返回引用、void的方法,注入null;对于返回原始类型的方法,注入0 |
@FieldValue | 注入被拦截对象的一个字段的值 |
@Morph | 类似于@SuperCall,但是允许指定调用参数 |
常用核心API
ByteBuddy
ElementMatchers(ElementMatcher)
DynamicType (动态类型,所有字节码操作的开始,非常值得关注)
Implementation (用于提供动态方法的实现)
Builder (用于创建DynamicType,相关接口以及实现后续待详解)
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) {
}
}
-javaagent:E:\itstack\git\github.com\MonitorDesign\cn-bugstack-middleware-bytebuddy\target\cn-bugstack-middleware-bytebuddy.jar
VM options
中,jar包地址按照自己的路径进行配置。监控 - 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
https://bytebuddy.net
,去了解更多关于 Byte Buddy
的内容。