字节码增强技术-ASM

概述

在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图所示:

WX20230814-215953@2x.png

使用字节码的好处:一处编译,到处运行。java 就是典型的使用字节码作为中间语言,在一个地方编译了源码,拿着.class 文件就可以在各种计算机运行。

字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。常见的字节码操作分为以下几类:

aop (1).png

优缺点如下:

字节码工具优点缺点
Java-proxy- 简单易用
- 原生支持
- 仅能代理接口或继承类
- 动态代理类需实现接口
ASM- 强大的字节码操作能力
- 高性能
- 学习曲线较陡
- 代码较复杂
AspectJ- 强大的 AOP 支持
- 高度集成
- 学习曲线较陡
- 需要特定编译器或处理器
JavaAssist- 提供高级别的 API
- 简化字节码操作
- 相对较简单
- 性能相对较低
CGLib- 可代理普通类
- 高性能
- 无法代理 final 类
- 生成代理类较大
ByteBuddy- 简洁的 API
- 高性能
- 功能全面
- 支持运行时生成和修改类
- 学习成本较高

ASM介绍

ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused onperformance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。下面我们看一下ASM是如何编辑class字节码的。

ASM处理流程

目标类 class bytes -> ClassReader 解析 -> ClassVisitor 增强修改字节码 -> ClassWriter 生成增强后的 class bytes -> 通过 Instrumentation 解析加载为新的 Class。

image.png

ASM API

ASM API 提供了两种与 Java 类交互以进行转换和生成的方式:基于事件的方式和基于树的方式。

包名描述
org.objectweb.asm提供一个小巧且快速的字节码操作框架。
org.objectweb.asm.commons提供一些有用的类和方法适配器。
org.objectweb.asm.signature提供对类型签名的支持。
org.objectweb.asm.tree提供一个构造所访问的类的树表示的 ASM 访问者。
org.objectweb.asm.tree.analysis基于 asm.tree 包提供了静态代码分析的框架。
org.objectweb.asm.util提供对于编程和调试目的有用的 ASM 访问者。 基于事件的 API
核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor
树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

利用ASM实现AOP

下面我们通过实践看下如何通过ASM实现增加方法,运行时修改方法:

例子

假设我们有一个类 MathUtils,其中有一个 add 方法,我们想要在方法执行前后添加日志。

public class MathUtils {public int add(int a, int b) {return a + b;}
}

为了利用ASM实现AOP,需要定义一个MathUtilsMethodVisitor类,用于对字节码的add方法进行修改
  
public class MathUtilsMethodAdapter extends ClassVisitor implements Opcodes {  public MathUtilsMethodAdapter(ClassVisitor cv) {  super(Opcodes.ASM4, cv);  }  @Override  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {  if (cv != null) {  cv.visit(version, access, name, signature, superName, interfaces);  }  }  @Override  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {  // 当方法名为add时进行修改if ("add".equals(name)) {  // 先得到原始的方法  MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);  MethodVisitor newMethod = null;  // 访问需要修改的方法  newMethod = new AsmMethodVisit(mv);  return newMethod;  }  if (cv != null) {  return cv.visitMethod(access, name, desc, signature, exceptions);  }  return null;  }  
}

定义AsmMethodVisit在进入方法时打印begin Entering method,返回时打印end Entering method

public class AsmMethodVisit extends MethodVisitor {  public AsmMethodVisit(MethodVisitor mv) {  super(Opcodes.ASM4, mv);  }  @Override  public void visitMethodInsn(int opcode, String owner, String name, String desc) {  super.visitMethodInsn(opcode, owner, name, desc);  }  @Override  public void visitCode() {  // 此方法在访问方法的头部时被访问到,仅被访问一次  // 此处可插入新的指令  super.visitCode();  mv.visitFieldInsn(org.springframework.asm.Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");  mv.visitLdcInsn("begin Entering method ");  mv.visitMethodInsn(org.springframework.asm.Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);  }  @Override  public void visitInsn(int opcode) {  // 此方法可以获取方法中每一条指令的操作类型,被访问多次  // 如应在方法结尾处添加新指令,则应判断:  if (opcode == Opcodes.IRETURN) {  // pushes the 'out' field (of type PrintStream) of the System class  mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");  // pushes the "Hello World!" String constant  mv.visitLdcInsn("end Entering method");  // invokes the 'println' method (defined in the PrintStream class)  mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");  }  super.visitInsn(opcode);  }  
}

最后,加个测试类MathUtilsTest,使用 ASM 生成一个add1的新方法,并在运行add方法时修改字节码来增强 add 方法,实现执行前后增加日志

public class MathUtilsTest extends ClassLoader implements Opcodes {  public static void main(String args[]) throws Exception {  ClassReader cr = new ClassReader(MathUtils.class.getName());  ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);  ClassVisitor cv = new MathUtilsMethodAdapter(cw);  cr.accept(cv, Opcodes.ASM4);  // 新增加一个方法  MethodVisitor mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "add1", "([Ljava/lang/String;)V", null, null);  mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");  mw.visitLdcInsn("this is add method print!");  mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");  mw.visitInsn(RETURN);  // this code uses a maximum of two stack elements and two local  // variables  mw.visitMaxs(0, 0);  mw.visitEnd();  byte[] code = cw.toByteArray();  MathUtilsTest loader = new MathUtilsTest();  Class<?> exampleClass = loader.defineClass(MathUtils.class.getName(), code, 0, code.length);  for (Method method : exampleClass.getMethods()) {  System.out.println(method);  }  System.out.println("***************************");  // 调用add方法,方法执行前后打印日志  Object result = exampleClass.getMethods()[1].invoke(exampleClass.newInstance(), 1, 2);  System.out.println(result);  // 将生成的MathUtils.class输出到磁盘,方便我们观察FileOutputStream fos = new FileOutputStream("/Users/xx/MathUtils.class");  fos.write(code);  fos.close();  }  
}

上述程序运行结果如下:

image.png

反编译生成的MathUtils.class,可以看到如下结果:

image.png

步骤总结

利用上面这个类实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

  • 首先通过MathUtilsMethodAdapter类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 <init> 后,将需要被增强的方法交给类AsmMethodVisit来进行处理。
  • 接下来,进入类AsmMethodVisit中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。 类AsmMethodVisit继续读取字节码指令,每当ASM访问到无参数指令时,都会调用AsmMethodVisit中的visitInsn方法。我们判断了当前指令是否为无参数的“IRETURN”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
  • 综上,重写AsmMethodVisit中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end Entering method”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

ASM工具

利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline

image.png

总结

Java ASM是一个强大的 Java 字节码操作框架,用于生成、修改和分析 Java 类的字节码。它允许您在不修改源代码的情况下,通过编程方式操作字节码,从而实现动态代码生成、AOP(面向切面编程)、字节码优化、代码注入等高级功能。ASM 可以在运行时或编译时进行字节码操作,它被广泛用于许多 Java 应用和框架中,如 Spring、Hibernate 等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/111250.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TCP/IP模型五层协议

TCP/IP模型五层协议 认识协议 约定双方进行的一种约定 协议分层 降低了学习和维护的成本&#xff08;封装&#xff09;灵活的针对这里的某一层协议进行替换 四/五层协议 五层协议的作用 应用层 应用层常见协议 应用层常见协议概览 基于TCP的协议 HTTP&#xff08;超…

AI绘画的魅力与未来:人工智能如何重塑艺术创作

随着人工智能&#xff08;AI&#xff09;技术的不断进步&#xff0c;AI绘画已经成为艺术与技术交汇的新领域。通过深度学习、神经网络等先进技术&#xff0c;AI不仅能理解和模拟人类艺术家的创作风格&#xff0c;还能生成令人惊叹的原创艺术作品。本文旨在探讨AI绘画的现状、挑…

ChatGPT AIGC自动生成多条件复杂计算函数

在Excel中经常会遇到多条件判断,根据不同的条件与内容显示不同的值。 例如: 需要给每个员工根据入职年限,员工等级,满意度等维度给员工发年终奖。 这在职场办公过程中经常要面临的一个问题。如销售额达到多少,取多少提成,如学生成绩在什么区间是设置为优秀还是良好等一…

python 对图片增加边框,logo贴图,获取图片exif参数,填写图片文本内容

完整代码 # 找到个可以下载免费字体的网站https://font.chi删除我naz.com/mi删除我anfei.html from PIL import Image, ImageDraw, ImageFont import exifreaddef photo_exif(image_path):f open(image_path, rb)tags exifread.process_file(f)# 打印所有照片信息&#xff0…

2023CRM排行:深度对比16款CRM

客户关系管理系统&#xff08;CRM&#xff09;作为数字化转型的重要载体&#xff0c;选择一个优秀的CRM系统将为企业未来健康增长保障。市场上CRM软件众多&#xff0c;但很难分清哪个适合自己&#xff0c;最近赶在公司选型&#xff0c;我对市场所有软件进行了一个调研&#xff…

单点登录是什么?

单点登录&#xff08;Single Sign On, SSO&#xff09;是指在同一帐号平台下的多个应用系统中&#xff0c;用户只需登录一次&#xff0c;即可访问所有相互信任的应用系统。 单点登录的本质就是在多个应用系统中共享登录状态。如果用户的登录状态是记录在 Session 中的&#xff…

异或运算.

相同为0&#xff0c;不同为1。 1 ^ 10 0 ^ 00 1 ^ 01 0 ^ 11性质&#xff1a; 0 ^ N N N ^ N 0交换、结合 a ^ b b ^ a&#xff1b; (a ^ b) ^ c a ^ (b ^ c)&#xff1b; 因此异或全部的元素的结果就是那个只出现1次的元素。 实现两个值的交换&#xff0c;而不必使…

C的魅力在于指针

原有的adrv9025 代理框架很好用,在其原有的平台上做改进

Python特征分析重要性的常用方法

前言 特征重要性分析用于了解每个特征(变量或输入)对于做出预测的有用性或价值。目标是确定对模型输出影响最大的最重要的特征&#xff0c;它是机器学习中经常使用的一种方法。 为什么特征重要性分析很重要? 如果有一个包含数十个甚至数百个特征的数据集&#xff0c;每个特征…

C#冒泡排序算法

冒泡排序实现原理 冒泡排序是一种简单的排序算法&#xff0c;其原理如下&#xff1a; 从待排序的数组的第一个元素开始&#xff0c;依次比较相邻的两个元素。 如果前面的元素大于后面的元素&#xff08;升序排序&#xff09;&#xff0c;则交换这两个元素的位置&#xff0c;使…

汽车屏类产品(四):仪表Cluster

###前言 仪表Cluster/仪表盘Dashboard,作为伴随汽车诞生就存在的一个主要零部件之一,从机械到电子到数字,可以说也是逐渐发展到现在的。 目前的主流框图如下,中间processor就是主控芯片,可能有buttons/switches,有display显示屏+backlight背光,有audio->speake…

线性代数-Python-01:向量的基本运算 -手写Vector -学习numpy的基本用法

文章目录 代码目录结构Vector.py_globals.pymain_vector.pymain_numpy_vector.py 一、创建属于自己的向量1.1 在控制台测试__repr__和__str__方法1.2 创建实例测试代码 二、向量的基本运算2.1 加法2.2 数量乘法2.3 向量运算的基本性质2.4 零向量2.5 向量的长度2.6 单位向量2.7 …

mybatis自定义类型控制器(TypeHandler)处理将字符串处理为集合

1. 问题&#xff1a; 假设这么一个场景 localurl里面的值大概这样&#xff1a;dwad21.jpg,dwad22.jpg,dwad.23.jpg 是一个字符串 如果我在sql表中有一个字段&#xff08;local_url&#xff09;是本地图片资源的多个url字符串拼接值。我想在java后端中不进行额外的转换就取值加…

【Qt控件之微调框、进度条】QSpinBox、QDoubleSpinBox、QDial、QProgressBar介绍及使用

概述 QSpinBox类提供了一个微调框小部件。 QSpinBox适用于处理整数和离散的值集&#xff08;例如&#xff0c;月份名称&#xff09;&#xff1b;对于浮点数值&#xff0c;请使用QDoubleSpinBox。 QSpinBox允许用户通过点击上下按钮或按键盘上的上下箭头来增加/减少当前显示的值…

Hadoop3教程(二十三):Yarn的三大调度器

文章目录 &#xff08;129&#xff09;FIFO调度器&#xff08;130&#xff09;容量调度器特点资源分配算法 &#xff08;131&#xff09;公平调度器特点缺额的定义队列资源分配方式基于FIFO策略基于Fair策略资源分配算法 DRF策略 参考文献 &#xff08;129&#xff09;FIFO调度…

搭建react项目

一、环境准备 1、安装node 官网下载安装&#xff1a;https://nodejs.org/en 注&#xff1a; npm5.2以后&#xff0c;安装node会自动安装npm和npx 2、安装webpack npm install -g webpack3、安装create-react-app npm install -g create-react-app二、创建react项目 1、初…

MSQL系列(六) Mysql实战-SQL语句优化

Mysql实战-SQL语句优化 前面我们讲解了索引的存储结构&#xff0c;BTree的索引结构&#xff0c;以及索引最左侧匹配原则&#xff0c;Explain的用法&#xff0c;可以看到是否使用了索引&#xff0c;今天我们讲解一下SQL语句的优化及如何优化 文章目录 Mysql实战-SQL语句优化1.…

GDPU 数据结构 天码行空5

一、实验目的 1&#xff0e;掌握队列的顺序存储结构 2&#xff0e;掌握队列先进先出运算原则在解决实际问题中的应用 二、实验内容 仿照教材顺序循环队列的例子&#xff0c;设计一个只使用队头指针和计数器的顺序循环队列抽象数据类型。其中操作包括&#xff1a;初始化、入队…

开关电源芯片好坏的判断标准是什么?如何判断电源芯片的好坏?

电源芯片是电子设备的一个重要元件&#xff0c;如果电源芯片损坏&#xff0c;那么设备也将停止工作。开关电源芯片是一种电源管理器件&#xff0c;可以将输入电压转换为稳定的输出电压。为了开关电源芯片可以正常稳定运行以及电子设备可以正常工作&#xff0c;对于电源芯片的检…

Python万圣节蝙蝠

目录 系列文章 前言 蝙蝠 程序设计 程序分析 运行结果 尾声 系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want5…