作者:小傅哥

博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!

一、前言

字节码编程插桩这种技术常与 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 进行字节码插桩,已达到我们的监控效果。

二、开发环境

  1. JDK 1.8.0
  2. javassist 3.12.1.GA
  3. 本章涉及源码在:itstack-demo-bytecode-1-04,可以关注公众号bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个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() 获取方法的标识,之后通过 与运算AccessFlag.STATIC,判断方法是否为静态方法。因为静态方法会影响后续的参数名称获取,静态方法第一个参数是 this ,需要排除。

1.5 方法:入参信息

CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
CtClass[] parameterTypes = ctMethod.getParameterTypes();
  • LocalVariableAttribute,获取方法的入参的名称。
  • parameterTypes,获取方法入参的类型。

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<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;
}

2.2 字节码插桩添加进入方法时间

// 定义属性
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);
}
}
  • 此时已经有了一个方法的开始时间,有了开始时间在加上后续的结尾时间。就可以很方便的统计一个方法的执行耗时。

2.3 字节码插桩添加入参输出

// 定义属性
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 进行插入,这里是为了更加清晰的向你展示字节码插桩的过程。现在我们就有了进入方法的时间和参数集合,方便后续输出。

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");
}
  • 这里一共有两个方法,一个用于记录正常情况下的监控信息。另外一个用于记录异常时候的信息。如果是实际的业务场景中,就可以通过这样的方法使用 MQ 将监控信息发送给服务端记录起来并做展示。

2.5 字节码插桩调用监控方法

// 方法后加强
ctMethod.insertAfter("{ org.itstack.demo.javassist.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
  • 这里通过静态方法将监控参数传递给外部;idxstartNanosparameterValues$_出参值

最终 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;
}
  • 现在已经可以将基本的监控信息传递给外部。对于一个普通的监控,如果不需要追踪链路,基本已经可以满足需求了。

2.6 字节码插桩给方法添加TryCatch

以上插桩内容,如果只是正常调用还是没问题的。但是如果方法抛出异常,那么这个时候就不能做到收集监控信息了。所以还需要给方法添加上 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 最开始执行就包裹原有方法内的内容,最后执行就包括所有内容。它依赖于顺序操作,其他的方法也是这样;insertBeforeinsertAfter

字节码编程,Javassist篇四《通过字节码插桩监控方法采集运行时入参出参和异常信息》的更多相关文章

  1. lua源码学习篇四:字节码指令

    在llimits.h文件中定义了指令的类型.其实就是32个字节. typedef lu_int32 Instruction; 上节说到变量最终会存入proto的数组k中,返回的索引放在expdesc ...

  2. java基础进阶篇(四)_HashMap------【java源码栈】

    目录 一.前言 二.特点和常见问题 二.接口定义 三.初始化构造函数 四.HashMap内部结构 五.HashMap的存储分析 六.HashMap的读取分析 七.常用方法 八.HashMap 的jav ...

  3. JDK源码学习--String篇(四) 终结篇

    StringBuilder和StringBuffer 前面讲到String是不可变的,如果需要可变的字符串将如何使用和操作呢?JAVA提供了连个操作可变字符串的类,StringBuilder和Stri ...

  4. 字节码编程,Javassist篇三《使用Javassist在运行时重新加载类「替换原方法输出不一样的结果」》

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 通过前面两篇 javassist 的基本内容,大体介绍了:类池(ClassPool) ...

  5. jvm虚拟机笔记<四> 虚拟机字节码执行引擎

    一.运行时栈帧结构 栈帧是用于支持虚拟机进行方法调用和执行的数据结构,是虚拟机栈的栈元素. 栈帧存储了局部变量表,操作数栈,动态连接,和返回地址等. 每一个方法的执行 对应的一个栈帧在虚拟机里面从入栈 ...

  6. Java字节码常量池深度剖析与字节码整体结构分解

    常量池深度剖析: 在上一次[https://www.cnblogs.com/webor2006/p/9416831.html]中已经将常量池分析到了2/3了,接着把剩下的分析完,先回顾一下我们编译的源 ...

  7. java编程思想第四版中net.mindview.util包下载,及源码简单导入使用

    在java编程思想第四版中需要使用net.mindview.util包,大家可以直接到http://www.mindviewinc.com/TIJ4/CodeInstructions.html 去下载 ...

  8. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

  9. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  10. jQuery2.x源码解析(构建篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...

随机推荐

  1. 从阿里云全球实时传输网络GRTN出发,浅谈QOE优化实践

    直播已深入每家每户,以淘宝的直播为例,在粉丝与主播的连麦互动中如何实现无感合屏或切屏?阿里云GRTN核心网技术负责人肖凯,在LVS2022上海站为我们分享了GRTN核心网的运作机制.运用方面以及QOE ...

  2. 【Boost】boost.log 要点笔记

    常用简写: namespace logging = boost::log; namespace src = boost::log::sources; namespace expr = boost::l ...

  3. 【算法学习笔记】区间DP

    基本的知识点引用自 OI wiki,感谢社区的帮助 什么是区间 DP? 区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系.令 ...

  4. 大数据(3)---HDFS客户端命令及java连接

    一.参数设置 之前有说到HDFS的备份数量和切块大小都是可以配置的,默认是备份3,切块大小默认128M 文件的切块大小和存储的副本数量,都是由客户端决定! 所谓的由客户端决定,是通过客户端机器上面的配 ...

  5. 你做的 9 件事表明你不是专业的 Python 开发人员

    本文转载自国外论坛 medium,原文地址: https://medium.com/navan-tech/7-java-features-you-might-not-have-heard-of-ade ...

  6. IDEA插件Material Theme UI 激活

    介绍 "Material Theme UI" 是一款为 IntelliJ IDEA 提供现代化材料设计主题的插件,通过重新设计IDE的外观,为开发人员带来更加美观.富有活力的用户体 ...

  7. python之单线程、多线程、多进程

    一.基本概念 进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础. 在当代面向线程设计的计算机结构中,进程是线程的容器.程 ...

  8. maven 工程pom依赖优化及常用命令

    本文为博主原创,转载请注明出处: 1. mvn dependency:list ---- 列出项目的所有jar包 mvn dependency:list -Dverbose 该命令可以列出项目依赖的所 ...

  9. 在线视频点播网站(python实现)

    本文将会对该项目进行一个简单的介绍,包括项目名称.项目背景.项目功能.技术栈等等. 项目名称 在线视频点播网站开发(python+django) 项目背景 学习完毕python和django之后,想找 ...

  10. [转帖]堆表&索引组织表

    堆表&索引组织表 https://zhuanlan.zhihu.com/p/487271927   15 人赞同了该文章 很多大佬强调学习一定要看"原版英文材料". 比如再 ...