文章采集调用( 开发环境JDK1.8.0javassist本章本章GA本章涉及源码的开发)
优采云 发布时间: 2022-04-13 14:15文章采集调用(
开发环境JDK1.8.0javassist本章本章GA本章涉及源码的开发)
一、前言
字节码编程插桩技术常结合Javaagent技术用于系统的非侵入式监控,可以替代方法中的硬编码操作。例如,您需要监控一个方法,包括;方法信息、执行时间、输入输出参数、执行链接、异常。那么就非常适合用这样的技术手段进行加工。
为了体现这部分的核心内容,本文将只使用Javassist技术对一段方法字节码进行instrument,最后输出该方法的执行信息,如下;
Method - 后续字节码增强操作的测试方法
public Integer strToInt(String str01, String str02) {
return Integer.parseInt(str01);
}
监控 - 方法的字节码增强后,输出监控信息
监控 - Begin
方法:org.itstack.demo.javassist.ApiTest.strToInt
入参:["str01","str02"] 入参[类型]:["java.lang.String","java.lang.String"] 入数[值]:["1","2"]
出参:java.lang.Integer 出参[值]:1
耗时:59(s)
监控 - End
有了这样的监控方案,我们基本上可以输出方法执行过程中的所有信息。然后通过后期的改进,将监控信息显示在界面上,并实时发出警报。不仅提高了系统的监控质量,也便于研发排查和定位问题。
这很好!然后我们一步步开始使用javassist进行字节码插桩,就达到了我们的监控效果。
二、开发环境JDK 1.8.0javassist 3.12.1.GA本章涉及的源码为:itstack-demo-bytecode -1-04,可以关注公众号:bugstack 虫洞栈,回复源码下载即可。您将获得下载链接列表。打开后第十七期“因为我有很多开源代码”,记得给个Star!三、技术实现1. 获取方法的基本信息1.1 获取类
ClassPool pool = ClassPool.getDefault();
// 获取类
CtClass ctClass = pool.get(org.itstack.demo.javassist.ApiTest.class.getName());
ctClass.replaceClassName("ApiTest", "ApiTest02");
String clazzName = ctClass.getName();
通过类名获取类信息,这里可以替换类名。它还包括一些其他操作来获取类中的属性,例如;ctClass.getSimpleName()、ctClass.getAnnotations() 等等。
1.2 如何获得
CtMethod ctMethod = ctClass.getDeclaredMethod("strToInt");
String methodName = ctMethod.getName();
通过getDeclaredMethod获取方法的CtMethod的内容。之后,您可以获取方法名称等信息。
1.3 方法信息
MethodInfo methodInfo = ctMethod.getMethodInfo();
MethodInfo 收录方法信息;名称、类型等
1.4 种方法类型
boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;
通过methodInfo.getAccessFlags()获取方法的标识符,然后使用AND运算AccessFlag.STATIC判断该方法是否为静态方法。因为静态方法会影响后续参数名的获取,所以静态方法的第一个参数就是this,需要排除。
1.5 方法:输入参数信息{name and type}
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
CtClass[] parameterTypes = ctMethod.getParameterTypes();
1.6 方法;参数信息
CtClass returnType = ctMethod.getReturnType();
String returnTypeName = returnType.getName();
对于方法的参数信息,只需要获取参数类型即可。
1.7 输出所有获取的信息
System.out.println("类名:" + clazzName);
System.out.println("方法:" + methodName);
System.out.println("类型:" + (isStatic ? "静态方法" : "非静态方法"));
System.out.println("描述:" + methodInfo.getDescriptor());
System.out.println("入参[名称]:" + attr.variableName(1) + "," + attr.variableName(2));
System.out.println("入参[类型]:" + parameterTypes[0].getName() + "," + parameterTypes[1].getName());
System.out.println("出参[类型]:" + returnTypeName);
输出结果
类名:org.itstack.demo.javassist.ApiTest
方法:strToInt
类型:非静态方法
描述:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;
入参[名称]:str01,str02
入参[类型]:java.lang.String,java.lang.String
出参[类型]:java.lang.Integer
以上,输出信息为监控方法做准备。从上面可以记录方法的基本描述和输入参数的数量。尤其是入参的个数,因为后面需要用到$1来获取没有给入参的值。
2. 方法字节码检测
需要通过字节码检测来更改的原创方法;
public class ApiTest {
public Integer strToInt(String str01, String str02) {
return Integer.parseInt(str01);
}
}
2.1 先标记基础属性
在监控的情况下,不可能将每次调用的所有方法信息都汇总输出。这不仅仅是性能问题,这些都是固定信息,不需要每次方法执行都输出。
这很好!然后在编译方法时,会为每个方法生成一个唯一的ID,并将方法的固定信息与ID关联起来。也可以通过ID将监控数据传递到外部。
// 方法:生成方法唯一标识ID
int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
生成ID的过程
public static final int MAX_NUM = 1024 * 32;
private final static AtomicInteger index = new AtomicInteger(0);
private final static AtomicReferenceArray methodTagArr = new AtomicReferenceArray(MAX_NUM);
public static int generateMethodId(String clazzName, String methodName, List parameterNameList, List parameterTypeList, String returnType) {
MethodDescription methodDescription = new MethodDescription();
methodDescription.setClazzName(clazzName);
methodDescription.setMethodName(methodName);
methodDescription.setParameterNameList(parameterNameList);
methodDescription.setParameterTypeList(parameterTypeList);
methodDescription.setReturnType(returnType);
int methodId = index.getAndIncrement();
if (methodId > MAX_NUM) return -1;
methodTagArr.set(methodId, methodDescription);
return methodId;
}
2.2 字节码检测增加入口方法时间
// 定义属性
ctMethod.addLocalVariable("startNanos", CtClass.longType);
// 方法前加强
ctMethod.insertBefore("{ startNanos = System.nanoTime(); }");
final类类方法
public class ApiTest {
public Integer strToInt(String str01, String str02) {
long startNanos = System.nanoTime();
return Integer.parseInt(str01);
}
}
2.3 字节码检测添加输入和输出
// 定义属性
ctMethod.addLocalVariable("parameterValues", pool.get(Object[].class.getName()));
// 方法前加强
ctMethod.insertBefore("{ parameterValues = new Object[]{" + parameters.toString() + "}; }");
final类类方法
public Integer strToInt(String str01, String str02) {
Object[] var10000 = new Object[]{str01, str02};
long startNanos = System.nanoTime();
return Integer.parseInt(str01);
}
2.4 定义监控方法
因为我们需要向外部输出监控信息。然后我们在这里定义一个静态方法,让字节码增强的方法调用,并输出监控信息。
public static void point(final int methodId, final long startNanos, Object[] parameterValues, Object returnValues) {
MethodDescription method = methodTagArr.get(methodId);
System.out.println("监控 - Begin");
System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName());
System.out.println("入参:" + JSON.toJSONString(method.getParameterNameList()) + " 入参[类型]:" + JSON.toJSONString(method.getParameterTypeList()) + " 入数[值]:" + JSON.toJSONString(parameterValues));
System.out.println("出参:" + method.getReturnType() + " 出参[值]:" + JSON.toJSONString(returnValues));
System.out.println("耗时:" + (System.nanoTime() - startNanos) / 1000000 + "(s)");
System.out.println("监控 - End\r\n");
}
public static void point(final int methodId, Throwable throwable) {
MethodDescription method = methodTagArr.get(methodId);
System.out.println("监控 - Begin");
System.out.println("方法:" + method.getClazzName() + "." + method.getMethodName());
System.out.println("异常:" + throwable.getMessage());
System.out.println("监控 - End\r\n");
}
2.5 字节码检测调用监控方法
// 方法后加强
ctMethod.insertAfter("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
final类类方法
public Integer strToInt(String str01, String str02) {
Object[] parameterValues = new Object[]{str01, str02};
long startNanos = System.nanoTime();
Integer var7 = Integer.parseInt(str01);
Monitor.point(0, startNanos, parameterValues, var7);
return var7;
}
2.6 字节码检测将 TryCatch 添加到方法中
以上instrumentation内容,如果只是正常调用,是没有问题的。但是如果方法抛出异常,那么此时就无法采集到监控信息。所以你还需要将 TryCatch 添加到方法中。
// 方法;添加TryCatch
ctMethod.addCatch("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception")); // 添加异常捕获
final类类方法
public Integer strToInt(String str01, String str02) {
try {
Object[] parameterValues = new Object[]{str01, str02};
long startNanos = System.nanoTime();
Integer var7 = Integer.parseInt(str01);
Monitor.point(0, startNanos, parameterValues, var7);
return var7;
} catch (Exception var9) {
Monitor.point(0, var9);
throw var9;
}
}
四、测试结果
下一步是执行我们的调用来测试修改后的方法字节码。通过不同的输入参数验证监测结果;
// 测试调用
byte[] bytes = ctClass.toBytecode();
Class clazzNew = new GenerateClazzMethod().defineClass("org.itstack.demo.javassist.ApiTest", bytes, 0, bytes.length);
// 反射获取 main 方法
Method method = clazzNew.getMethod("strToInt", String.class, String.class);
Object obj_01 = method.invoke(clazzNew.newInstance(), "1", "2");
System.out.println("正确入参:" + obj_01);
Object obj_02 = method.invoke(clazzNew.newInstance(), "a", "b");
System.out.println("异常入参:" + obj_02);
测试结果
监控 - Begin
方法:org.itstack.demo.javassist.ApiTest.strToInt
入参:["str01","str02"] 入参[类型]:["java.lang.String","java.lang.String"] 入数[值]:["1","2"]
出参:java.lang.Integer 出参[值]:1
耗时:63(s)
监控 - End
正确入参:1
监控 - Begin
方法:org.itstack.demo.javassist.ApiTest.strToInt
异常:For input string: "a"
监控 - End
五、总结