做者:小傅哥
博客:https://bugstack.cn - 系列专题文章编写
java
沉淀、分享、成长,让本身和他人都能有所收获!
字节码编程插桩这种技术常与 Javaagent
技术结合用在系统的非入侵监控中,这样就能够替代在方法中进行硬编码操做。好比,你须要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就很是适合使用这样的技术手段进行处理。编程
为了能让这部分最核心的内容体现出来,本文会只使用 Javassist
技术对一段方法字节码进行插桩操做,最终输出这段方法的执行信息,以下;数组
方法 - 测试方法用于后续进行字节码加强操做框架
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
进行字节码插桩,已达到咱们的监控效果。this
itstack-demo-bytecode-1-04
,能够关注公众号:bugstack虫洞栈
,回复源码下载获取。你会得到一个下载连接列表,打开后里面的第17个「由于我有好多开源代码」
,记得给个Star
!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()
等。编码
CtMethod ctMethod = ctClass.getDeclaredMethod("strToInt"); String methodName = ctMethod.getName();
经过 getDeclaredMethod 获取方法的 CtMethod
的内容。以后就能够获取方法的名称等信息。spa
MethodInfo methodInfo = ctMethod.getMethodInfo();
MethodInfo 中包括了方法的信息;名称、类型等内容。code
boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;
经过 methodInfo.getAccessFlags()
获取方法的标识,以后经过 与运算,AccessFlag.STATIC
,判断方法是否为静态方法。由于静态方法会影响后续的参数名称获取,静态方法第一个参数是 this
,须要排除。
CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); CtClass[] parameterTypes = ctMethod.getParameterTypes();
CtClass returnType = ctMethod.getReturnType(); String returnTypeName = returnType.getName();
对于方法的出参信息,只须要获取出参类型。
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
,来获取没有给入参的值。
一段需会被字节码插桩改变的原始方法;
public class ApiTest { public Integer strToInt(String str01, String str02) { return Integer.parseInt(str01); } }
在监控的适合,不可能每一次调用都把全部方法信息汇总输出出来。这样作不仅是性能问题,而是这些都是固定不变的信息,没有必要让每一次方法执行都输出。
好!那么在方法编译时候,给每个方法都生成一个惟一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<MethodDescription> methodTagArr = new AtomicReferenceArray<>(MAX_NUM); public static int generateMethodId(String clazzName, String methodName, List<String> parameterNameList, List<String> 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; }
// 定义属性 ctMethod.addLocalVariable("startNanos", CtClass.longType); // 方法前增强 ctMethod.insertBefore("{ startNanos = System.nanoTime(); }");
long
类型的属性,startNanos
。并经过 insertBefore
插入到方法内容的开始处。最终 class
类方法
public class ApiTest { public Integer strToInt(String str01, String str02) { long startNanos = System.nanoTime(); return Integer.parseInt(str01); } }
// 定义属性 ctMethod.addLocalVariable("parameterValues", pool.get(Object[].class.getName())); // 方法前增强 ctMethod.insertBefore("{ parameterValues = new Object[]{" + parameters.toString() + "}; }");
Object[]
,用于记录入参信息。最终 class
类方法
public Integer strToInt(String str01, String str02) { Object[] var10000 = new Object[]{str01, str02}; long startNanos = System.nanoTime(); return Integer.parseInt(str01); }
insertBefore
进行插入,这里是为了更加清晰的向你展现字节码插桩的过程。如今咱们就有了进入方法的时间和参数集合,方便后续输出。由于咱们须要将监控信息,输出给外部。那么咱们这里会定义一个静态方法,让字节码加强后的方法去调用,输出监控信息。
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"); }
MQ
将监控信息发送给服务端记录起来并作展现。// 方法后增强 ctMethod.insertAfter("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 若是返回类型非对象类型,$_ 须要进行类型转换
idx
、startNanos
、parameterValues
、$_
出参值 最终 class
类方法
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; }
以上插桩内容,若是只是正常调用仍是没问题的。可是若是方法抛出异常,那么这个时候就不能作到收集监控信息了。因此还须要给方法添加上 TryCatch
。
// 方法;添加TryCatch ctMethod.addCatch("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception")); // 添加异常捕获
addCatch
将方法包装在 TryCatch
里面。catch
中调用外部方法,将异常信息输出。$e
,用于获取抛出异常的内容。最终 class
类方法
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);
ClassLoader
加载字节码,以后生成新的类。测试结果
监控 - 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
Javassist
字节码操做框架能够很是方便的去进行字节码加强,也不须要考虑纯字节码编程下的指令码控制。但若是考虑性能以及更加细致的改变,仍是须要使用到 ASM
。这里包括一些字节码操做的知识点,以下;
methodInfo.getDescriptor()
,能够输出方法描述信息。(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Integer;
,其实就是方法的出入参和返回值。$1 $2 ...
用于获取不一样位置的参数。$$
能够获取所有入参,可是不太适合用在数值传递中。this
参数。AccessFlag.STATIC。addCatch
最开始执行就包裹原有方法内的内容,最后执行就包括全部内容。它依赖于顺序操做,其余的方法也是这样;insertBefore
、insertAfter
。