目录
1、前言
2、什么是ASM?
2.1、工作流程
2.2、ASM集合核心API
2.1.1、ClassReader
2.1.2、ClassWriter
2.1.3、 ClassVisitor
2.1.4、MethodVisitor
2.1.5、 FieldVisitor
2.1.6、Opcodes
3、简单示例
3.1、maven依赖
3.2、hello world
3.3、执行结果
4、ASM和Javassist
4.1、操作层级
4.2、性能
4.3、应用场景
1、前言
在上一篇Javassist入门中,我们介绍了如何使用Javassist操作Java字节码,Javassist主要是利用Java源码以及反射机制来实现的。而今天将要介绍另一种能操作Java字节码的技术,也就是ASM。他相比Javassist更灵活,提供了更细粒度的控制。
2、什么是ASM?
ASM是一个通用的 Java 字节码操作和分析框架。它可用于修改现有类或动态生成类(直接以二进制形式)。ASM 提供了一些常见的字节码转换和分析算法,可从中构建自定义复杂转换和代码分析工具。ASM 提供与其他 Java 字节码框架类似的功能,但更注重性能。由于它的设计和实现尽可能小巧和快速,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
官网地址:ASM
2.1、工作流程
- 基于访问者模式:
- ASM 使用访问者模式来遍历和修改字节码。ClassReader 类负责读取class文件并通知给 ClassVisitor 的实现。
- ClassVisitor 接收来自 ClassReader 的事件,并可以选择性地将这些事件传递给下一个 ClassVisitor 或者修改字节码内容。
- MethodVisitor 和 FieldVisitor 分别用于处理方法和字段相关的字节码信息。
- 字节码流解析:
- ClassReader 会把 class 文件的内容解析成一系列的字节码指令流。
- 这些指令流按照 Java 虚拟机规范定义的格式进行组织,包括常量池、访问标志、字段表、方法表等结构。
- 字节码生成与转换:
- ClassWriter 类是 ASM 中用来生成新的 class 文件的核心组件。
- 它实现了 ClassVisitor 接口,在接收到各种字节码元素后,根据需要构建出新的字节码序列。
- 在这个过程中可以插入、删除或修改原有的字节码指令,从而实现对类行为的动态调整。
流程图如下:
2.2、ASM集合核心API
2.1.1、ClassReader
ClassReader 用于读取 .class 文件的字节码,解析其内容并提供访问类结构的方法。常用的方法有:
- Classreader(InputStream):从输入流读取类字节码。
- accept(ClassVisitor visitor, int flags):将类的字节码委托给ClassVisitior进行访问和处理,其中flags参数有:
- ClassReader.EXPAND_FRAMES: 自动计算和修复栈帧。
- ClassReader.SKIP_DEBUG: 跳过调试信息。
- ClassReader.SKIP_FRAMES: 跳过栈帧信息。
使用方式:
java">ClassReader classReader = new ClassReader("java.lang.String");
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
2.1.2、ClassWriter
ClassWriter 用于生成或修改类的字节码,并输出为字节数组,甚至可以生成新的字节码文件。常用的方法有:
- ClassWriter(int flags):创建 ClassWriter 对象。其中flags参数有:
- ClassWriter.COMPUTE_MAXS:自动计算方法的最大栈深度。
- ClassWriter.COMPUTE_FRAMES:自动计算方法的栈帧。
- toByteArray():将生成的字节码转换为字节数组。
使用方式:
java">ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
byte[] modifiedClass = classWriter.toByteArray();
2.1.3、 ClassVisitor
ClassVisitor 是访问类结构的核心接口,所有对类的操作都需要通过它完成。常用的方法有:
- visit: 访问类的基本信息(版本号、类名等)。
- visitMethod:访问类中的方法。
- visitField: 访问类中的字段。
- visitEnd: 访问结束。
使用方式:
java">ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {System.out.println("Class name:" + name);}
};
2.1.4、MethodVisitor
MethodVisitor 用于访问和修改方法的字节码指令。常用的方法有:
- visitCode(): 方法开始时调用。
- visitInsn(int opcode):访问无操作数的指令。
- visitVarInsn(int opcode, int var):访问局部变量相关指令。
- visitLdcInsn(Object value): 插入一个常量。
- visitEnd():方法结束时调用。
使用方式:
java">MethodVisitor methodVisitor = new MethodVisitor(Opcodes.ASM9) {@Overridepublic void visitCode() {super.visitCode();mv.visitLdcInsn("Hello, ASM!");mv.visitInsn(Opcodes.ARETURN);}
};
2.1.5、 FieldVisitor
FieldVisitor 用于访问和修改类中的字段。常用方法有:
- visitAnnotation(String descriptor, boolean visible):访问字段上的注解。
- visitEnd():访问字段结束。
使用方式:
java">FieldVisitor fieldVisitor = new FieldVisitor(Opcodes.ASM9) {@Overridepublic void visitEnd() {System.out.println("Field visit finished.");}
};
2.1.6、Opcodes
Opcodes 是 ASM 提供的一组常量,用于表示字节码中的操作码、访问标志等。常用的常量有:
- 类访问标志:
- Opcodes.ACC_PUBLIC:表示公共访问权限。
- Opcodes.ACC_FINAL:表示不可继承。
- 方法操作码:
- Opcodes.RETURN:表示方法返回。
- Opcodes.INVOKEVIRTUAL:表示调用实例方法。
- 字段操作码:
- Opcodes.GETFIELD:表示获取字段的值。
- Opcodes.PUTFIELD:表示设置字段的值。
使用方式:
java">MethodVisitor mv = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "myMethod", "()V", null, null);
3、简单示例
3.1、maven依赖
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.7.1</version>
</dependency>
3.2、hello world
使用ASM操作字节码,创建一个MyClass类,并构造一个方法,方法打印hello world, i am from asm信息。
java">package org.example.asm;import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class AsmDemo {public static void main(String[] args) throws Exception {// 创建一个 ClassWriter 实例,用于生成类的字节码ClassWriter classWriter = new ClassWriter(0);// 定义一个新的类 MyClass// Opcodes.V1_8 表示 Java 8 的版本号// Opcodes.ACC_PUBLIC 表示类的访问权限为 publicclassWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "MyClass", null, "java/lang/Object", null);// 添加默认的构造函数// Opcodes.ACC_PUBLIC 表示构造函数的访问权限为 public// <init> 表示构造函数的名称,必须为 <init>。在 Java 源代码中,编译器会自动为类生成构造方法(如果未显式声明),生成的构造方法在字节码中始终以 <init> 作为名称。<init> 是 JVM 规范中固定表示构造方法的名称。// ()V 表示构造函数的参数类型为空,即该方法无参MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);constructor.visitCode();// visitVarInsn 用于操作局部变量的字节码指令方法// Opcodes.ALOAD 表示加载一个局部变量到操作数栈上,0表示当前对象 thisconstructor.visitVarInsn(Opcodes.ALOAD, 0);// Opcodes.INVOKESPECIAL 表示调用一个特殊方法,这里调用父类 Object 的构造函数constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);constructor.visitInsn(Opcodes.RETURN); // 返回constructor.visitMaxs(1, 1); // 设置操作数栈和局部变量表的最大深度constructor.visitEnd(); // 结束构造函数的定义// 添加 sayHello 方法,同上构造函数创建MethodVisitor mv = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sayHello", "()V", null, null);mv.visitCode();// 获取 System.out// Opcodes.GETSTATIC 表示从静态字段中获取值,这里获取 System.out// Ljava/io/PrintStream; 表示 PrintStream 类型mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");// 调用 PrintStream.println 方法mv.visitLdcInsn("Hello world, i am from ASM!"); // 将字符串 "Hello from ASM!" 压入操作数栈mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 调用 println 方法mv.visitInsn(Opcodes.RETURN); // 返回mv.visitMaxs(2, 2); // 设置操作数栈和局部变量表的最大深度mv.visitEnd(); // 结束 sayHello 方法的定义// 完成类定义classWriter.visitEnd();// 获取生成的类的字节码byte[] classData = classWriter.toByteArray();// 创建自定义类加载器MyClassLoader classLoader = new MyClassLoader();// 使用自定义类加载器加载生成的类Class<?> myClass = classLoader.defineClass("MyClass", classData);// 创建 MyClass 的实例Object instance = myClass.getDeclaredConstructor().newInstance();// 调用 MyClass 的 sayHello 方法myClass.getMethod("sayHello").invoke(instance);}// 自定义类加载器static class MyClassLoader extends ClassLoader {public Class<?> defineClass(String name, byte[] data) {// 使用父类的 defineClass 方法定义类return defineClass(name, data, 0, data.length);}}
}
3.3、执行结果
4、ASM和Javassist
上面介绍了基本的ASM用法以及API,在上一篇中讲述了Javassist的方式操作字节码。同样操作字节码的技术,我们对比一下两者的区别以及联系。
4.1、操作层级
ASM:基于字节码指令的低级别操作,直接操作字节码,接近 JVM 的底层实现。开发者需要熟悉 JVM 字节码的结构(例如操作数栈、局部变量表、指令等),更灵活但也更复杂。
Javassist:基于高层级的 API,提供类似 Java 源代码的操作方式,无需直接理解和操作字节码指令。更加高抽象,适合快速开发动态字节码功能,易于理解和使用。
4.2、性能
ASM:性能更高,因为它直接操作字节码,无额外的抽象层。更适合性能敏感的场景,例如框架底层实现或对运行时性能要求非常高的工具。
Javassist:性能略低于 ASM,因为其高层级 API 会引入一定的开销。
4.3、应用场景
ASM:用于开发高性能框架和工具,例如 AOP 框架、性能监控工具等;需要对字节码做精细控制。适合在Spring、MyBatis 等框架使用 ASM 提供底层的字节码增强能力。
Javassist:用于快速开发动态字节码功能,例如动态代理、简单方法增强;更适合业务层代码增强场景。适合快速开发,例如动态生成 POJO 类、简单的性能监控工具等。
综上,其实不难可以看出,ASM更接近于字节码底层的操作手法,天然的更具备灵活性,但是相应的代码的可读性和学习难度也较高。而Javassist更多像是个二方包,将底层字节码的操作方式封装为可读性更强的API,更方便开发者进行调用。