作者:小傅哥
博客:https://bugstack.cn
Wiki:https://github.com/fuzhengwei/CodeGuide/wiki

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

一、前言

你开发的系统是裸奔的吗?深夜被老板 Diss

一套系统是否稳定运行,取决于它的运行健康度,而这包括;调用量可用率响应时长以及服务器性能等各项指标的一个综合值。并且在系统出现异常问题时,可以抓取整个业务方法执行链路并输出;当时的入参、出参、异常信息等等。当然还包括一些JVM、Redis、Mysql的各项性能指标,以用于快速定位并解决问题。

那么要做到这样的事情有什么监控方案呢,这里面的做法比较多。比如;

  1. 最简单粗暴的可能就是硬编码在方法中,收取执行耗时以及出入参和异常信息。但这样的成本实在太大,而且有一些不可预估的风险。
  2. 可以选择切面方式做一套统一监控的组件,相对来说还是好一些的。但也需要硬编码,同时维护成本不低。
  3. 市面上对于这样的监控其实是有整套的非入侵监控方案的,比如;Google DapperZipkin等都可以实现,他们都是基于探针技术非入侵的采用字节码增强的方式进行监控。

,那么这样非入侵的探针方式是怎么实现的呢?如何去做方法的字节码增强

在字节码增强方面有三个框架;ASMJavassistByteCode,各有优缺点按需选择。这在我们之前的字节码编程文章里也有所提到。

本文主要讲解关于 ASM 方式的字节码增强,接下来的案例会逐步讲解一个给方法添加 TryCatch 块,用于采集异常信息以及正常的出参结果的流程。

一步步向你展示通过指令码来改写你的方法!

二、系统环境

  1. jdk1.8.0
  2. asm-commons 6.2.1

三、技术目标

通过 ASM 字节码增强技术,使用指令码将方法修改为我们想要的效果。这部分原本需要使用 JavaAgent 技术,在工程启动加载时候进行修改字节码。这里为了将关于字节码核心内容展示出来,通过加载类名称获取字节码进行修改。

这是修改之前的方法

public Integer strToNumber(String str) {return Integer.parseInt(str);
}

这是修改之后的方法

public Integer strToNumber(String str) {try {Integer var2 = Integer.parseInt(str);MethodTest.point("org.itstack.test.MethodTest$Test.strToNumber", var2);return var2;} catch (Exception var3) {MethodTest.point("org.itstack.test.MethodTest$Test.strToNumber", var3);throw var3;}
}

从修改前到修改后,可以看到。有如下几点修改;

  1. 返回值赋值给新的参数,并做了输出
  2. 把方法包裹在一个 TryCatch 中,并将异常也做了输出

好!如果你有很敏锐的嗅觉,或者很多小问号。那么你是否会想到如果使用到你自己的业务中,是不是就可以做一套非入侵的监控系统了? 之后升职加薪

四、实现过程

字节码增强的过程乍一看还是比较麻烦的,如果你没有阅读过JVM虚拟机规范等相关书籍,确实很不好理解。但是也就是这部分不那么容易理解的知识,才是你后续价值的体现。

接下来我会一步步的带着你通过字节码增强的方式,来实现我们的监控需求。最终的完整的代码,可以通过关注公众号bugstack虫洞栈 回复源码获取(ASM字节码编程)。

1. 搭建字节码框架

/*** 字节码增强获取新的字节码*/
private byte[] getBytes(String className) throws IOException {ClassReader cr = new ClassReader(className);ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);cr.accept(new ClassVisitor(ASM5, cw) {public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {// 方法过滤if (!"strToNumber".equals(name))return super.visitMethod(access, name, descriptor, signature, exceptions);MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);return new AdviceAdapter(ASM5, mv, access, name, descriptor) {// 方法进入时修改字节码                                          protected void onMethodEnter() {}// 访问局部变量和操作数栈public void visitMaxs(int maxStack, int maxLocals) {}// 方法退出时修改字节码  protected void onMethodExit(int opcode) {}};}}, ClassReader.EXPAND_FRAMES);return cw.toByteArray();
}

以上这段代码就是 ASM 用于处理字节码增强的模版代码块。首先他会分别创建 ClassReaderClassWriter,用于对类的加载和写入,这里的加载方式在构造方法中也提供的比较丰富。可以通过类名、字节码或者流的方式进行处理。

接下来是对方法的访问 MethodVisitor ,基本所有使用 ASM 技术的监控系统,都会在这里来实现字节码的注入。这里面目前用到了三个方法的,如下;

  1. onMethodEnter 方法进入时设置一些基本内容,比如当前纳秒用于后续监控方法的执行耗时。还有就是一些 Try 块的开始。
  2. visitMaxs 这个是在方法结束前,用于添加 Catch 块。到这也就可以将整个方法进行包裹起来了。
  3. onMethodExit 最后是这个方法退出时,用于 RETURN 之前,可以注入结尾的字节码加强,比如调用外部方法输出监控信息。

基本上所有的 ASM 字节码增强操作,都离不开这三个方法。下面我就一步步来用指令将方法改造。

2. 获取方法返回值

这是一个被测试的方法;

public Integer strToNumber(String str) {return Integer.parseInt(str);
}

编写指令

这个 onMethodExit 方法就是我们上面提到的字节码编写框架中的内容,在里面添加具体的字节码指令。

@Override
protected void onMethodExit(int opcode) {if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {int nextLocal = this.nextLocal;mv.visitVarInsn(ASTORE, nextLocal); // 将栈顶引用类型值保存到局部变量indexbyte中。mv.visitVarInsn(ALOAD, nextLocal);  // 从局部变量indexbyte中装载引用类型值入栈。}
}
  1. this.nextLocal,获取局部变量的索引值。这个值就让局部变量最后的值,也就是存放 ARETURN 的值(ARETURN,是返回对象类型,如果是返回 int 则需要使用 IRETURN)。
  2. ASTORE,将栈顶引用类型值保存到局部变量indexbyte中。这里就是把返回的结果,保存到局部变量。在你头脑中可以想象这有两块区域,一个是局部变量、一个是操作数栈。他们不断的进行压栈和操作
  3. ALOAD,从局部变量indexbyte中装载引用类型值入栈。现在再将这个值放到操作数栈用,用于一会输出使用。

被初次增强后的方法;

public Integer strToNumber(String str) {Integer var2 = Integer.parseInt(str);return var2;
}
  • 首先可以看到,原本的返回值被赋值到一个参数上,之后再由 return 将参数返回。这样也就可以让我们拿到了方法出参 var2 进行输出操作。

3. 输出方法返回值

在上面我们已经将返回内容赋值给参数,那么在 return 之前,我们就可以在添加一个方法来输出方法信息和出参了。

定义输出结果方法;

public static void point(String methodName, Object response) {System.out.println("系统监控 :: [方法名称:" + methodName + " 输出信息:" + JSON.toJSONString(response) + "]\r\n");
}

接下来我们使用字节码增强的方式来调用这个静态方法。

@Override
protected void onMethodExit(int opcode) {if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {...mv.visitLdcInsn(className + "." + name);  // 类名.方法名mv.visitVarInsn(ALOAD, nextLocal);mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Object;)V", false);}
}
  1. mv.visitLdcInsn(className + “.” + name);,常量池中的常量值(int, float, string reference, object reference)入栈。也就是我们把类名和方法名,写到常量池中。
  2. mv.visitVarInsn(ALOAD, nextLocal);,将上面我们提到的返回值加载到操作数栈。
  3. mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), “point”, “(Ljava/lang/String;Ljava/lang/Object;)V”, false);,调用静态方法。INVOKESTATIC 是调用指令,后面是方法的地址、方法名、方法描述。
  4. (Ljava/lang/String;Ljava/lang/Object;)V,表示 StringObject 类型的入参,V 是返回空。整体看也就是我们的方法;void point(String methodName, Object response)

再次被增强后的方法;

public Integer strToNumber(String str) {Integer var2 = Integer.parseInt(str);point("org.itstack.test.MethodTest.strToNumber", var2);return var2;
}
  • 在字节码增强后的方法,每次调用这个方法都会输出方法的名称和出参结果。可能还有一个问题就是,如果抛异常了,那么就监控不到了!

4. 给方法加上TryCatch

如果需要抓住方法的异常信息并输出,那么就需要给原有的方法包上一层 TryCatch 捕获异常。接下来我们开始完成这样的指令码操作。

添加 TryCatch 开始

private Label from = new Label(),to = new Label(),target = new Label();@Override
protected void onMethodEnter() {//标志:try块开始位置visitLabel(from);visitTryCatchBlock(from,to,target,"java/lang/Exception");
}
  • onMethodEnter() 中,加入 TryCatch 开始块,在部分在 ASM 中固定的模式,按照需求添加即可。

添加 TryCatch 结尾

@Override
public void visitMaxs(int maxStack, int maxLocals) {//标志:try块结束mv.visitLabel(to);//标志:catch块开始位置mv.visitLabel(target);mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});  // 异常信息保存到局部变量int local = newLocal(Type.LONG_TYPE);mv.visitVarInsn(ASTORE, local);// 抛出异常mv.visitVarInsn(ALOAD, local);mv.visitInsn(ATHROW);super.visitMaxs(maxStack, maxLocals);
}
  • visitMaxs 方法中完成 TryCatch 的结尾,包住异常请抛出。
  • mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});,在指定方法操作数栈中将 TryCatch 处理完成。这里面的几个参数也可以动态拼装;局部变量、参数、栈、异常。
  • ASTORE,将异常信息保存到局部变量,并使用指定 ALOAD 放到操作数栈,用于抛出。
  • ATHROW,最后是抛出异常的指令,也就是 throw var;

这次增强后的方法;

public Integer strToNumber(String str) {try {Integer var2 = Integer.parseInt(str);point("org.itstack.test.MethodTest.strToNumber", var2);return var2;} catch (Exception var3) {throw var3;}
}
  • 这时离我们要的内容越来越近了,整个方法被包装到一个 TryCatch 中,并按照需要输出我们的信息。接下来就需要将异常信息,打印出来。

5. 输出异常信息

在我们使用 ASM 字节码增强后,已经可以将方法拓展的非常的适合于监控了。接下来我们定义一个静态方法,用于输出异常信息;

定义输出异常方法;

public static void point(String methodName, Throwable throwable) {System.out.println("系统监控 :: [方法名称:" + methodName + " 异常信息:" + throwable.getMessage() + "]\r\n");
}

接下来的事情就很简单了,只需要在抛出异常的指令中,把调用外部方法的内容集成进去就可以了。

@Override
public void visitMaxs(int maxStack, int maxLocals) {...// 输出信息mv.visitLdcInsn(className + "." + name);  // 类名.方法名mv.visitVarInsn(ALOAD, local);mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);...
}
  • 这一部分主要体现将异常信息进行输出,通过字节码指令来实现调用外部方法。
  • mv.visitLdcInsn,加载常量。也就是类名和方法名。
  • ALOAD,将异常信息加载到操作数栈用,用于输出。
  • INVOKESTATIC,调用静态方法。调用方法除了这个指令外还有;invokespecialinvokevirtualinvokeinterface

现在再看字节码增强后的方法;

public Integer strToNumber(String str) {try {Integer var2 = Integer.parseInt(str);point("org.itstack.test.MethodTest.strToNumber", (Object)var2);return var2;} catch (Exception var3) {point("org.itstack.test.MethodTest.strToNumber", (Throwable)var3);throw var3;}
}

好!到这我们已经将这个方法彻底的通过字节码改造完成,可以非常方便的监控异常信息。对用外部输出的方法,后续可以通过 MQ 等机制推送出去,用于图表展示监控信息。

五、测试验证

这是一个字符串转换成数字类型的方法,我们通过调用传输不同的参数进行验证。比如;数字类型字符串和非数字类型字符串。

另外这里是我们通过字节码增强的方式进行改造方法,改造后这个方法反馈给我们的仍然是字节码,所以需要使用到 ClassLoader 进行加载到执行。

测试方法;

public static void main(String[] args) throws Exception {// 方法字节码增强byte[] bytes = new MethodTest().getBytes(MethodTest.class.getName());// 输出方法新字节码outputClazz(bytes, MethodTest.class.getSimpleName());    // 测试方法Class<?> clazz = new MethodTest().defineClass("org.itstack.test.MethodTest", bytes, 0, bytes.length);Method queryUserInfo = clazz.getMethod("strToNumber", String.class);            // 正确入参;测试验证结果输出Object obj01 = queryUserInfo.invoke(clazz.newInstance(), "123");System.out.println("01 测试结果:" + obj01);   // 异常入参;测试验证打印异常信息Object obj02 = queryUserInfo.invoke(clazz.newInstance(), "abc");System.out.println("02 测试结果:" + obj02);
}

输出结果;

ASM字节码增强后类输出路径:/User/itstack/git/github.com/WormholePistachio/SQM/target/test-classes/MethodTestSQM.class系统监控 :: [方法名称:org.itstack.test.MethodTest.strToNumber 输出信息:123]01 测试结果:123
系统监控 :: [方法名称:org.itstack.test.MethodTest.strToNumber 异常信息:For input string: "abc"]         Process finished with exit code 1

六、总结

  • 通过字节码指令控制代码的编写注入,是不是很酷?完成功能的同时,逐步也解了 JVM虚拟机 。至少不向以前那样只是去硬背一些理论,而是彻底的实践了。不要感觉这很难,嗯!
  • 在逐步的了解字节码编程后,你会在很多的场景领域中建设出高级的玩法。甚至去翻看源码也能更加容易阅读理解,并把这技巧复用给自己其他系统。
  • 比如我们常用的非入侵的监控系统,全链路监控,以及一些反射框架中,其实都用到了 ASM,只是还没有注意到而已。最终多学习一些延申拓展的知识,关于这些技巧可以阅读 JVM虚拟机规范,也可以阅读ASM文档;asm.itstack.org

七、彩蛋

最近将个人原创代码库资源整理出一份 wiki 文档,同时逐步将各类案例汇总集中,方便获取。

本代码库是作者小傅哥多年从事一线互联网Java开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享,给个Star ✨)!

链接:https://github.com/fuzhengwei/CodeGuide/wiki


ASM字节码编程 | 用字节码增强技术给所有方法加上TryCatch捕获异常并输出相关推荐

  1. 灰度重采样的方法分为_DSA医疗影像增强技术特点及方法

    医学是关系到千千万万人的身心健康的应用学科,医学的发展水平体现了一个国家的人民生活标准,代表了一个国家的综合国力.自古以来,我国传统医学讲究"望.闻.问.切"这些最基本的手段,但是 ...

  2. 字节码编程,Byte-buddy篇二《监控方法执行耗时动态获取出入参类型和值》

    作者:小傅哥 博客:https://bugstack.cn - 汇总系列专题文章 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 案例是剥去外衣包装展示出核心功能的最佳学习方式! 就像是我们研 ...

  3. 字节码编程 | 工作多年的你依然重复做着CRUD?是否接触过这种技术?

    大家好,我是冰河~~ 最近和不少小伙伴聊天,发现大部分小伙伴,其中可能就包括正在看文章的你和我,工作时间已经不短了,有些小伙伴工作3~5年了,有些甚至超过8年了. 但是大部分小伙伴平时的工作都是在简单 ...

  4. aop 获取方法入参出参_ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称及入参和出参结果并记录方法耗时...

    作者:小傅哥 博客:bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获! ❞ 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了 ...

  5. ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时

    作者:小傅哥 博客:bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了多少代码 ...

  6. JAVA字节码增强技术之ASM

    ASM是一款基于java字节码层面的代码分析和修改工具:无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析,代码优化和代码混淆等工作.ASM的目标是生成,转换和分析已编译的java ...

  7. idea如何反编译字节码指令_美团点评:Java字节码增强技术,线上问题诊断利器...

    作者简介:泽恩,美团到店住宿业务研发团队工程师.文章转载于公众号:美团技术团队 1. 字节码 1.1 什么是字节码? Java之所以可以"一次编译,到处运行",一是因为JVM针对各 ...

  8. JVM插桩之一:JVM字节码增强技术介绍及入门示例

    字节码增强技术:AOP技术其实就是字节码增强技术,JVM提供的动态代理追根究底也是字节码增强技术. 目的:在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修 ...

  9. 字节码增强技术 Byte Buddy 、Javassist、Instrumentation

    概述 字节码增强技术 有 Byte Buddy .Javassist等多种. 如果是在同一个包中,没有问题,不需借助Instrumentation 如果是第三方包,想不修改代码的情况下实现代理技术,就 ...

最新文章

  1. Docker学习(7)——常用的镜像构建方式简介
  2. NOIP2009潜伏者【B003】
  3. python猜单词游戏_磁盘空间不足。
  4. Makefile学习之路——2
  5. 行高 line-height
  6. SQLServer2008-镜像数据库实施手册(双机)SQL-Server2014同样适用
  7. .NET Core开发实战(第32课:集成事件:解决跨微服务的最终一致性)--学习笔记...
  8. java正则效率_善用Pattern提高你的应用处理正则表达式的效率(Java)
  9. m1芯片MacBook Air安装arm版MacTeX及配置
  10. php 实现资料下载功能,学习猿地-php如何实现下载功能
  11. 关于动态数组指针操作的两个例子
  12. StanfordDB class自学笔记 (3) 查询关系型数据库总览
  13. 各国个人信息安全立法进度
  14. 教你七招记单词快又准
  15. 惠普总裁口述的职业规划(3)
  16. 摄像机高精度标定的一些方法
  17. mysql 联合主键的作用
  18. 时序分析 19 VAR(Vector Autoregression) 向量自回归
  19. c# 讯飞语音 sdk
  20. 安卓apk修改(Android反编译apk)

热门文章

  1. 软件测试计算公式总结
  2. csgo下方各种数据都是意思_比MySQL还好用的数据库,不会都不好意思。
  3. OFD文档标准 3.根节点文档
  4. iPhone强制关机
  5. Python删除Excel中的指定工作簿Sheet
  6. 从《剑与家园》中学习战斗策略设计
  7. 数据库查询,返回前5、10行数据
  8. Switch搭配“廉价采集卡”时,稍稍降低延迟的设置方法
  9. html需要电脑什么配置,玩大型游戏需要什么电脑配置?大型游戏电脑配置推荐...
  10. Java实现 LeetCode 638 大礼包(阅读理解题,DFS)