1. ASM简介
我们知道程序的分析。生成和转换是很有用的技术,可以用于很多场景。ASM作为一个Java字节码处理工具,它被设计用于处理已编译的Java类。ASM不是生成和转变已编译的Java类的唯一工具,但它是最新且最有效的工具之一。特点是体积小,速度快,开源。它的作用和JVM的动态性相辅相成,在许多场景下有很好的表现,例如相比于AOP的编译期织入,ASM的操作速度更快。
2. ASM导入
2.1 maven导入
<properties>
<asm.version>9.1</asm.version>
</properties>
<dependencies>
<dependency>
<artifactId>asm</artifactId>
<groupId>org.ow2.asm</groupId>
<version>${asm.version}</version>
</dependency>
<dependency>
<artifactId>asm-commons</artifactId>
<groupId>org.ow2.asm</groupId>
<version>${asm.version}</version>
</dependency>
</dependencies>
2.2 gradle导入
dependencies {
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}
3. 字节码结构
3.1 Class字节码文件结构
说明 | 长度 | 个数 | |
---|---|---|---|
魔数 | 识别文件格式 | u4 | 1 |
版本号 | 主版本号(大版本) | u2 | 1 |
副版本号(小版本) | u2 | 1 | |
常量池集合 | 常量池计数器 | u2 | 1 |
常量池表 | n | 计数器-1 | |
访问标识 | 访问标识符 | u2 | 1 |
索引集合 | 类索引 | u2 | 1 |
父类索引 | u2 | 1 | |
接口计数器 | u2 | 1 | |
接口索引表 | n | 接口计数器 | |
字段表集合(Field) | 字段计数器 | u2 | 1 |
字段表 | n | 字段计数器 | |
方法表集合(Method) | 方法计数器 | u2 | 1 |
方法表 | n | 方法计数器 | |
属性表集合 | 属性计数器 | u2 | 1 |
属性表 | n | 属性计数器 |
3.2 版本号对应关系
主版本 | 副版本 | 编译器版本 | ASM版本 |
---|---|---|---|
45 | 3 | 1.1 | V1_1 |
46 | 0 | 1.2 | V1_2 |
47 | 0 | 1.3 | V1_3 |
48 | 0 | 1.4 | V1_4 |
49 | 0 | 1.5 | V1_5 |
50 | 0 | 1.6 | V1_6 |
51 | 0 | 1.7 | V1_7 |
52 | 0 | 1.8 | V1_8 |
53 | 0 | 1.9 | V9 |
54 | 0 | 1.10 | V10 |
后续的依照表规律类推,Java的版本号从45开始,JDK1.1之后的每个JDK大版本发布主版本号向上加1。
不同版本的Java编译器编译的Class文件的版本是不同的。高版本的JVM可以执行低版本编译器生成的Class文件,否则不行,并抛出 java.lang.UnsupportedClassVersionError 异常。
3.3 类型描述符
描述符的作用用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)以及返回值。对应关系如下:
Java类型 | 类型描述符 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
3.4 方法描述符
方法描述符是一个类型描述符的列表,它在单个字符串中描述了一个方法的参数类型和返回类型。传入参数由括号包围,紧接着为返回类型的类型描述符。例如如下几种示例;
源文件中的方法声明 | 方法描述符 |
---|---|
void m(int i, float f) | (IF)V |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i, String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
3.5 访问标识符
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标志为public类型 |
ACC_FINAL | 0x0010 | 标志被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法) |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应) |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
当有多个访问标识符来修饰时,采用加法结合(或者 | 或运算),比如 public final 修饰的类,其标记为 ACC_PUBLIC | ACC_FINAL,其值为 0x0011,对应的十进制为 17 。
补充
- 带有 ACC_INTERFACE 标志的 class 文件表示的是接口而不是类
- 如果一个class文件被设置了 ACC_INTERFACE 标志,同时也得设置 ACC_ABSTRACT标志。同时它不能再设置ACC_FINAL、ACC_SUPER 或ACC_ENUM标志。
- 如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
- ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义,Java虚拟机默认为每个class文件都设置了 ACC_SUPER 标志
- ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
- 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
- ACC_ENUM标志表明该类或其父类为枚举类型。
4. ASM 解析类信息
4.1 ASM组件
ASM提供了基于 ClassVisitor API的三个基本组件用来生成/转换class:
- ClassReader: 用于解析class文件,通过 accept 方法可以开始解析,并调用传入的 ClassVisitor 的 visitXxx 方法回调解析结果。ClassReader可以认为是解析事件的生产者。
- ClassWriter: 这是 ClassVisitor 的一个实现类。它可以生成class字节码文件的byte数组,生成的byte数组可以通过 toByteArray 方法获取。ClassWriter可以认为是解析事件的消费者
- ClassVisitor: 它所接收到的方法调用需要委托给另一个 ClassVisitor 实例。ClassVisitor可以看做是解析事件的过滤器。
4.2 解析一个类 - ClassReader + ClassVisitor
根据4.1 ASM 组件的介绍,我们知道:ClassReader作为解析事件的发生者,用于读取类信息。ClassVisitor作为解析事件的访问者/过滤器,可以接收来自ClassReader发来的回调,并将这个回调转发给其他ClassVisitor实例,访问到当前ClassReader读取到的类信息。ClassVisitor是一个抽象类,提供了各种访问信息的回调接口,列出最基本的几个内容:
public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,
String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
void visitEnd();
}
实现一个ClassVisitor的子类ClassPrinter来打印读取到的类信息,这个 ClassPrinter 并不做任何过滤操作,仅将所有回调内容都打印出来:
public class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(Opcodes.ASM4);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println("----visit");
System.out.println(version+" "+access+" "+name+" "+signature+" "+superName+" "+ Arrays.toString(interfaces));
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public void visitSource(String source, String debug) {
System.out.println("----visitSource");
System.out.println(source+" "+debug);
super.visitSource(source, debug);
}
@Override
public void visitOuterClass(String owner, String name, String descriptor) {
System.out.println("----visitOuterClass");
System.out.println(owner+" "+name+" "+descriptor);
super.visitOuterClass(owner, name, descriptor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println("----visitAnnotations");
System.out.println(descriptor+" "+visible);
return super.visitAnnotation(descriptor, visible);
}
@Override
public void visitAttribute(Attribute attribute) {
System.out.println("----visitAttribute");
System.out.println(attribute);
super.visitAttribute(attribute);
}
@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
System.out.println("----visitInnerClass");
System.out.println(name+" "+outerName+" "+innerName+" "+access);
super.visitInnerClass(name, outerName, innerName, access);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("----visitField");
System.out.println(access+" "+name+" "+descriptor+" "+signature+" "+value);
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("----visitMethod");
System.out.println(access+" "+name+" "+descriptor+" "+signature+" "+ Arrays.toString(exceptions));
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
@Override
public void visitEnd() {
System.out.println("----visitEnd");
super.visitEnd();
}
}
我们定义一个很简单的类:
@MyAnnotation("value")
public class ASMTest {
public int num;
public final int getNum() {
return num;
}
private void setNum(int num) {
this.num = num;
}
}
结合ClassReader和ClassPrinter来解析它:
//ClassVisitor的实现类,打印所有visitXxx的回调参数
ClassPrinter cp = new ClassPrinter();
try {
//ClassReader是类信息的读取者,解析事件的产生者
ClassReader cr = new ClassReader("asmcore.base.ASMTest");
//通过accept方法开始解析,并在解析到对应元素时回调传入的cp的visitXxx方法
cr.accept(cp,0);
} catch (IOException e) {
e.printStackTrace();
}
得到解析的结果:
----visit
52 33 asmcore/base/ASMTest null java/lang/Object []
----visitSource
ASMTest.java null
----visitAnnotations
Lasmcore/base/MyAnnotation; true
----visitField
1 num I null null
----visitMethod
1 <init> ()V null null
----visitMethod
17 getNum ()I null null
----visitMethod
2 setNum (I)V null null
----visitEnd
Process finished with exit code 0
可以看到,class文件的所有关键信息都被打印了出来。需要我们注意的是 visitXxx 方法的回调关系(先后回调顺序为表格从上到下):
方法名 | 调用次数 |
---|---|
visit | 1 |
visitSource | 最多一次 |
visitOuterClass | 最多一次 |
(visitAnnotation | visitAttribute) | 0次或更多 |
(visitInnerClass | visitField | visitMethod) | 0次或更多 |
visitEnd | 1 |
其中(A|B)形式表示A和B的出现顺序不是保证有先后的。但表格中从上到下整体的顺序是严格的回调先后的顺序。
4.3 生成一个类 - ClassWriter
我们知道 ClassWriter 继承自 ClassVisitor,它的任务是将访问回调的结果保存到byte数组中,直到 visitEnd 方法被回调表示解析/访问事件结束,也就意味着一个Class文件对应的byte数组数据也就保存完成,可以通过 ClassWriter 的 toByteArray 方法获取到保存的byte数组。
我们假装自己是一个 ClassReader,严格根据 4.2 总结的回调顺序回调 ClassWriter 的 visitXxx 顺序,由此来让 ClassWriter 去构建一个类:
ClassWriter cw = new ClassWriter(0);
//先模拟类访问开始
//package pkg;
//public interface Comparable extends Measure
cw.visit(V1_8,
ACC_PUBLIC|ACC_ABSTRACT|ACC_INTERFACE,
"Comparable",
null,
"java/lang/Object",
new String[]{"Measurable"});
//模拟访问回调字段信息
//int TAG = 0; //接口中的字段描述符默认为 public final static
cw.visitField(ACC_PUBLIC|ACC_FINAL|ACC_STATIC,
"TAG","I",null,0)
.visitEnd();
//模拟访问回调方法信息
//int compareTo(Object o); //接口中方法描述符默认为 public abstract
cw.visitMethod(ACC_PUBLIC|ACC_ABSTRACT,
"compareTo","(Ljava/lang/Object;)I",null,null)
.visitEnd();
//模拟类访问结束
cw.visitEnd();
//这个byte[]就是class文件的字节流了
byte[] b = cw.toByteArray();
最后得到的字节码反编译后结果为:
public interface Comparable extends Measurable {
int TAG = 0;
int compareTo(Object var1);
}
需要注意的是 visitMethod 和 visitField 之后需要跟上 visitEnd,这两个方法的返回值是 MethodVisitor 和 FieldVisitor 实例,他们事件访问结束的标识为 visitEnd 的回调。
4.4 使用生成的class文件字节流 - ClassLoader
我们回顾到类加载机制,JVM通过类加载器,将class文件加载入JVM,class文件的来源可以有很多,本地或者网络,只要是符合规范的class文件,就可以通过字节流的方式进行读取,ClassLoader的 defineClass 方法将byte[]加载为Class对象。
public class MyClassLoader extends ClassLoader {
public MyClassLoader(ClassLoader parent){
super(parent);
}
public Class<?> generateClass(String name,byte[] b){
return defineClass(name,b,0,b.length);
}
}
需要注意这里的类加载器需要传入父类加载器,如果要加载的class文件中使用了项目中其他自定义类,则要通过双亲委派机制让父类加载器去完成加载。
4.5 删除、添加类成员 - ClassReader+ClassVisitor+ClassWriter
根据前面的介绍,我们知道 ClassVisitor 可以作为一个事件的过滤器,这句话的意思可以理解为,ClassVisitor 访问事件的回调可以进行分发!它有点像装饰器的代码增强,给出下面的代码或许可以更好的描述我的意思:
public class ClassVisitorFilter extends ClassVisitor{
//父类的 ClassVisitor cv 可以理解为装饰器的被装饰者
public ClassVisitorFilter(ClassVisitor downstream){
super(V1_8,downstream);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value){
//before
pre();
//转发事件到被装饰者的该方法
FiledVisitor fv = super.visitField(access,name,descriptor,signature,value);
//不对被装饰者/下游分发该方法,将上述代码注释掉,如下:
//FiledVisitor fv = super.visitField(access,name,descriptor,signature,value);
//after
after();
return fv;
}
}
我们给一个类结构如下:
package asmcore.base;
public class Student {
public String name;
public String getName(){
return name;
}
}
我们现在要增加一个 int 类型的字段 age,并删除 name 字段和 getName 方法。我们首先通过 ClassReader 发起一个解析事件,并将回调交给事件过滤器 ClassVisitor,我们这里把它命名为 XxxAdapter,通过责任链模式的过滤处理后,最后将事件转发给最后的事件处理器 ClassWriter。流程图如下:
针对字段增加和删除的过滤处理,我们写一个 ClassVisitor 的实现类 FieldAdapter:
public class FieldAdapter extends ClassVisitor {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
private boolean isAdd;
public FieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc,boolean isAdd) {
super(ASM4,cv);
this.fAcc = fAcc;
this.fName= fName;
this.fDesc = fDesc;
this.isAdd = isAdd;
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
//-------代码增强---------
if (!isAdd && name.equals(fName)){
//如果不是添加,就将这个field删除
return null;
}
//如果是添加field,就先判断这个field是否出现过,如果没有出现过,最后可以在visitEnd的时候添加
if (name.equals(fName)){
isFieldPresent = true;
}
//-------代码增强---------
//回调给下一层
return cv.visitField(access, name, descriptor, signature, value);
}
@Override
public void visitEnd() {
if (!isFieldPresent && isAdd){
FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
if (fv != null){
fv.visitEnd();
}
}
super.visitEnd();
}
}
需要注意的是我们要避免重名的字段出现,所以要在扫描过所有字段过后,确保没有该字段出现,才往其中添加目标字段。visitEnd 方法的回调可以确保之前 visitField 已经完全结束,此时再转发给下游 visitField() 模拟真的访问到了要添加的目标字段,从而达到添加字段的效果。
类似的思路,我们可以定义一个删除方法的事件过滤器,其实就是不向下游转发 visitMethod 即可,就达成了模拟没有访问到该方法的效果。
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter(ClassVisitor cv,String name,String desc) {
super(ASM4,cv);
this.mName = name;
this.mDesc = desc;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
//--------代码增强--------
if (name.equals(mName) && descriptor.equals(mDesc)){
//如果是要删除的方法,则不回调 MethodVisitor
return null;
}
//--------代码增强--------
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
}
这是装饰器模式,配合责任链,我们可以实现过滤处理:
ClassWriter cw = new ClassWriter(0);
//添加一个 age 字段
FieldAdapter addFieldAdapter = new FieldAdapter(cw, ACC_PUBLIC, "age", "I",true);
//删除一个 name 字段
FieldAdapter removeFieldAdapter = new FieldAdapter(addFieldAdapter,ACC_PUBLIC,"name","Ljava/lang/String;",false);
//删除一个 getName 方法
RemoveMethodAdapter removeMethodAdapter = new RemoveMethodAdapter(removeFieldAdapter, "getName", "()Ljava/lang/String;");
try {
ClassReader cr = new ClassReader("asmcore.base.Student");
//装饰器责任链
cr.accept(removeMethodAdapter,0);
byte[] bytes = cw.toByteArray();
save(bytes,"Student");
} catch (IOException e) {
e.printStackTrace();
}
最后我们得到处理后的Student类字节码文件:
package asmcore.base;
public class Student {
public int age;
public Student() {
}
}
可以发现成功地把 name 字段和 getName 方法删除,并添加了 int 类型的 age 字段。