Java Agent简单实例:方法监控
本篇文章主要介绍通过Java Agent和Javassist技术实现方法的自动监控,监控方法的参数值、返回结果以及方法耗时等信息,有助于我们快速复现并排查相关问题,提高系统稳定性
在开始阅读本篇文章之前,希望读者可以了解Java Agent和Javassist的基本原理和使用方法,有助于我们更加快速理解本篇文章的内容
0. 为什么使用Java Agent
在实际开发过程中,如果想要进行方法的监控,我们可以通过硬编码或者AOP完成这个需求;但是,在微服务的场景下,我们可能有成千上万个服务,每个服务又存在各种各样的方法,如果要对所有服务进行监控,我们可能需要在每个服务中都编写AOP的代码,这样的代码相当冗余并且耗时
如果我们开发一个监听方法的SDK,直接让目标服务引入我们的SDK进行方法监控,其实也能解决问题;但是我们需要与各个服务的负责人沟通,推荐他们引入SDK,这才是真正耗时的事情,可能要很久都无法取得很好的效果
那么有没有什么技术可以帮助我们简化这个操作呢,答案就是Java Agent。Java探针技术相当于JVM层面的AOP,我们不需要再对一个个的服务进行AOP编程,只需要通过Java探针,使用一个jar包和-javaagent命令参数来完成所有方法的监控,只需要经过简单的测试就能完成方法监控
1. Javassist-方法修改
我们的目的是对方法执行监控,那么首先就是确定在监控期间我们需要方法执行的哪些信息,然后通过方法的修改帮助我们获得这些信息,完成我们的目标;进而将方法的运行信息打印或者输出到日志中心(如es),从而实现方法的监控
1.1 监控信息
对于方法的监控,我们需要的监控信息大致如下:
- 方法名称
- 方法入参类型&方法入参
- 方法返回值类型&返回值(非void)
- 方法执行耗时
- 异常信息(假如方法执行过程中发生异常)
对于方法的监控大概需要上述信息
通过这些信息我们可以了解一个方法执行过程中所接收到的参数和执行结果,并了解到方法的耗时;如果发生异常还可以查询方法的异常信息。我们可以掌握方法每次执行的详细信息,如果发生问题可以轻松复现并寻找解决方法
1.2 实现细节
上小节中确定了方法监控所需要的各种信息,在本小节将讨论具体的实现细节
首先,我们需要获取方法的名称、入参类型和返回值类型,对于每一个方法来说这些信息是不变的,而且方法每次运行时监控都需要这些信息,那么我们可以将这些信息缓存起来,避免每次重复加载,提高效率
其次,方法入参值、方法返回值和方法耗时这些信息在方法的每次执行时都是不一样的,所以我们需要在方法执行过程中动态地获取这些参数并传递到我们自定义的方法监控方法
最后,为了简单起见,在本篇文章中,只是将这些监控信息打印出来,并未将其上传到统一的日志中心
要想将监控信息上传到统一的日志中心,那么就要为每个方法分布一个唯一的methodId,用于唯一标识该方法,可以通过雪花算法等方式生成这个methodId,这里直接使用AtomicInteger来生成methodId
在分布式情况下,上面提到的固定信息的缓存可以只缓存在本地,而不需要通过统一的缓存;因为每个服务只会调用服务本身的方法,方法内部的RPC或http调用不在当前服务的方法监控范围内
1.3 实际编码
项目实际结构如下:
code
监控方法:
public class BlogService {
/**
* 获取博客内容
* @param id 博客id
* @param author 博客作者
* @return
*/
public String getBlog(Integer id, String author) {
System.out.println("博客ID: " + id);
System.out.println("博客作者: " + author);
return "blog's content!";
}
}
自定义描述方法详情的类:
public class MethodDescription {
private String className;
private String methodName;
private List<String> parameterNameList;
private List<String> parameterTypeList;
private String returnType;
public MethodDescription() {
}
public MethodDescription(String className, String methodName, List<String> parameterNameList,
List<String> parameterTypeList, String returnType) {
this.className = className;
this.methodName = methodName;
this.parameterNameList = parameterNameList;
this.parameterTypeList = parameterTypeList;
this.returnType = returnType;
}
// 省略getter和setter方法
}
用于生成methodId的方法:
public static final int MAX_NUM = 1024 * 32;
private final static AtomicInteger index = new AtomicInteger(0);
/**
* key: hashcode
* value: methodId
*/
private final static Map<Integer, Integer> methodInfos = new ConcurrentHashMap<>();
// 在分布式环境下使用ConcurrentHashMap缓存方法详情
// private final static Map<Integer, MethodDescription> methodTagAttr = new ConcurrentHashMap<>();
// 简单起见,这里使用AtomicReferenceArray缓存方法详情
private final static AtomicReferenceArray<MethodDescription> methodTagArr = new AtomicReferenceArray<>(MAX_NUM);
public static int generateMethodId(Integer hashcode, String clazzName, Strin