文章采集调用( 开发环境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

  五、总结

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线