0%

ASM笔记

序言

累土而不辍,丘山崇成。

今天来学习磨人的ASM框架。

简介

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

简单的说,ASM可以读取解析class文件内容,并提供接口让你可以对class文件字节码内容进行CRUD操作。

注: class文件存储的是java字节码,ASM 是对java字节码操作的一层封装,因此,如果你很了解 class文件格式的话,你甚至可以通过直接使用文本编辑器(eg:Vim)来改写class文件。

知道了 ASM 的作用后,接下来就来看下 ASM 的执行模式。

ASM框架执行流程

ASM 提供了两组API:Core和Tree:

  • Core是基于访问者模式来操作类的
  • Tree是基于树节点来操作类的

本文我们主要讨论的是 ASM 的 CoreAPI。

ASM 内部采用 访问者模式.class 类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor内部相应的方法。
比如:

  • 扫描到类文件时,会回调ClassVisitorvisit()方法;
  • 扫描到类注解时,会回调ClassVisitorvisitAnnotation()方法;
  • 扫描到类成员时,会回调ClassVisitorvisitField()方法;
  • 扫描到类方法时,会回调ClassVisitorvisitMethod()方法;
    ······
    扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()返回MethodVisitor实例),通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor字节码内容覆盖原来.class文件就实现了类文件的代码切入。

具体关系如下:

树形关系 使用的接口
Class ClassVisitor
Field FieldVisitor
Method MethodVisitor
Annotation AnnotationVisitor

整个具体的执行时序如下图所示:

image-20200509145250530

通过时序图可以看出ASM在处理class文件的整个过程。ASM通过树这种数据结构来表示复杂的字节码结构,并利用 Push模型 来对树进行遍历。

  • ASM 中提供一个ClassReader类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept()方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。

  • 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。

  • 通常,cr.accept(cvv, skip_Debug).

    cvv通常是来继承ClassVistor的一个类的实例,这个类里面通常重载ClassVisitor的一系列方法,实现我们自己的操作。

接下来还剩的一点内容就是如何实现class文件字节码的修改。

ASM字节码修改

由于 ASM 是直接对class文件的字节码进行操作,因此,要修改class文件内容时,也要注入相应的java字节码。

所以,在注入字节码之前,我们还需要了解下class文件的结构,JVM指令等知识。

Class文件结构

Java源文件经过javac编译器编译之后,将会生成对应的二进制.class文件,如下图所示:

image-20200509153518513

Java类文件是 8 位字节的二进制流。数据项按顺序存储在class文件中,相邻的项之间没有间隔,这使得class文件变得紧凑,减少存储空间。在Java类文件中包含了许多大小不同的项,由于每一项的结构都有严格规定,这使得 class 文件能够从头到尾被顺利地解析。

每个class文件都是有固定的结构信息,而且保留了源码文件中的符号。下图是class文件的格式图。其中带 * 号的表示可重复的结构。

image-20200509153606292

类结构体中所有的修饰符、字符常量和其他常量都被存储在class文件开始的一个常量堆栈(Constant Stack)中,其他结构体通过索引引用。

每个类必须包含headers(包括:class name, super class, interface, etc.)和常量堆栈(Constant Stack)其他元素,例如:字段(fields)、方法(methods)和全部属性(attributes)可以选择显示或者不显示。

每个字段块(Field section)包括名称、修饰符(public, private, etc.)、描述符号(descriptor)和字段属性。

每个方法区域(Method section)里面的信息与header部分的信息类似,信息关于最大堆栈(max stack)和最大本地变量数量(max local variable numbers)被用于修改字节码。对于非abstract和非native的方法有一个方法指令表,exceptions表和代码属性表。除此之外,还可以有其他方法属性。

每个类、字段、方法和方法代码的属性有属于自己的名称记录在类文件格式的JVM规范的部分,这些属性展示了字节码多方面的信息,例如源文件名、内部类、签名、代码行数、本地变量表和注释。JVM规范允许定义自定义属性,这些属性会被标准的VM(虚拟机)忽略,但是可以包含附件信息。

方法代码表包含一系列对java虚拟机的指令。有些指令在代码中使用偏移量,当指令从方法代码被插入或者移除时,全部偏移量的值可能需要调整。

Java类型与Class文件内部类型对应关系

Java类型分为基本类型和引用类型,在 JVM 中对每一种类型都有与之相对应的类型描述,如下表:

Java type JVM Type descriptor
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;

ASM 中要获得一个类的 JVM 内部描述,可以使用org.objectweb.asm.Type类中的getDescriptor(final Class c)方法,如下:

1
2
3
4
5
6
public class TypeDescriptors {    
public static void main(String[] args) {
System.out.println(Type.getDescriptor(TypeDescriptors.class));
System.out.println(Type.getDescriptor(String.class));
}
}

运行结果:

1
2
Lorg/victorzhzh/core/structure/TypeDescriptors;    
Ljava/lang/String;

Java方法声明与Class内部声明的对应关系

在·Java·的二进制文件中,方法的方法名和方法的描述都是存储在Constant pool 中的,且在两个不同的单元里。因此,方法描述中不含有方法名,只含有参数类型和返回类型。

格式:(参数描述符)返回值描述符

Method declaration in source file Method descriptor
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;
String m() ()Ljava/lang/String;

JVM指令

假设现在我们有如下一个类:

image-20200509154551909

先用javac com/yn/test/Test.java编译得到Test.class文件,然后再使用javap -c com/yn/test/Test来查看下这个Test.class文件的字节码,结果如下图所示:

image-20200509154617771

上图中第3行到第7行,是类Test的默认构造函数(由编译器默认生成),Code以下部分是构造函数内部代码,其中:

aload_0: 这个指令是LOAD系列指令中的一个,它的意思表示装载当前第 0 个元素到堆栈中。代码上相当于this。而这个数据元素的类型是一个引用类型。这些指令包含了:ALOAD,ILOAD,LLOAD,FLOAD,DLOAD。区分它们的作用就是针对不用数据类型而准备的LOAD指令,此外还有专门负责处理数组的指令 SALOAD。

invokespecial: 这个指令是调用系列指令中的一个。其目的是调用对象类的方法。后面需要给上父类的方法完整签名。“#1”的意思是 .class 文件常量表中第1个元素。值为:“java/lang/Object.”“:()V”。结合ALOAD_0。这两个指令可以翻译为:“super()”。其含义是调用自己的父类构造方法。

第9到14行是main方法,Code以下是其字节码表示:

getstatic: 这个指令是GET系列指令中的一个其作用是获取静态字段内容到堆栈中。这一系列指令包括了:GETFIELD、GETSTATIC。它们分别用于获取动态字段和静态字段。此处表示的意思获取静态成员System.out到堆栈中。

ldc:这个指令的功能是从常量表中装载一个数据到堆栈中。此处表示从常量池中获取字符串”Hello World!”。

invokevirtual:也是一种调用指令,这个指令区别与 invokespecial 的是它是根据引用调用对象类的方法。此处表示调用java.io.PrintStream.println(String)方法,结合前面的操作,这里调用的就是System.out.println("Hello World!")

return: 这也是一系列指令中的一个,其目的是方法调用完毕返回:可用的其他指令有:IRETURN,DRETURN,ARETURN等,用于表示不同类型参数的返回。

接下来,我们就可以根据上面所讲的内容,将代码字节码注入到class文件中了。

现在假设我们想要在类Testmain方法前后动态插入代码,如下所示:

image-20200509154949319

要完成在main方法前后插入输出代码,需要以下几步操作:

读取Test.class文件,可以通过 ASM 提供的ClassReader类进行class文件的读取与遍历。

1
2
3
4
5
6
7
8
9
10
11
// 使用全限定名,创建一个ClassReader对象
ClassReader classReader = new ClassReader("com.yn.test.Test");

// 构建一个ClassWriter对象,并设置让系统自动计算栈和本地变量大小
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

//创建一个自定义ClassVisitor,方便后续ClassReader的遍历通知
ClassVisitor classVisitor = new TestClassVisitor(classWriter);

//开始扫描class文件
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);

javap反编译得到的字节码可以知道,实现System.out.println("Hello World!");的字节码总共需要3步操作:
(1). 获取System静态成员out,其对应的指令为getstatic,对应的 ASM 代码为:

1
2
3
4
5
mv.visitFieldInsn(Opcodes.GETSTATIC,
Type.getInternalName(System.class), //"java/lang/System"
"out",
Type.getDescriptor(PrintStream.class) //"Ljava/io/PrintStream;"
);

(2). 获取字符串常量”Hello World!”,其对应的指令为ldc,对应的 ASM 代码为:

1
mv.visitLdcInsn("Hello World!");

(3). 获取PrintStream.println(String)方法,其对应的指令为invokervirtual,对应的 ASM 代码为:

1
2
3
4
5
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
Type.getInternalName(PrintStream.class), //"java/io/PrintStream"
"println",
"(Ljava/lang/String;)V",//方法描述符
false);

main方法进入前,进行代码插入,可以通过MethodVisitor.visitCode()方法。

1
2
3
4
5
6
7
// 在源方法前去修改方法内容,这部分的修改将加载源方法的字节码之前
@Override
public void visitCode() {
mv.visitCode();
System.out.println("method start to insert code");
sop("asm insert before");//this is the insert code
}

main方法退出前,进行代码插入,可以通关过MethodVisitor.visitInsn()方法,通过判断当前的指令为return时,表明即将执行return语句,此时插入字节码即可。

1
2
3
4
5
6
7
8
9
10
@Override
public void visitInsn(int opcode) {
//检测到return语句
if (opcode == Opcodes.RETURN) {
System.out.println("method end to insert code");
sop("asm insert after");
}
//执行原本语句
mv.visitInsn(opcode);
}

字节码插入class文件成功后,导出字节码到原文件中。

1
2
3
4
5
6
7
//获取改写后的class二进制字节码
byte[] classFile = classWriter.toByteArray();
// 将这个类输出到原先的类文件目录下,这是原先的类文件已经被修改
File file = new File("E:/code/Android/Projects/AsmButterknife/sample-java/build/classes/java/main/com/yn/test/Test.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(classFile);
fos.close();

注: asm-commons 包中提供了一个类AdviceAdapter,使用该类可以更加方便的让我们在方法前后注入代码,因为其提供了方法onMethodEnter()onMethodExit()

通过上面介绍的内容,我们已经成功使用 ASM 动态注入字节码到class文件中。但是如果直接采用 ASM 代码注入字节码,还是相对困难的,幸运的是 ASM 给我们提供了 ASMifier 工具,使得我们可以直接通过.class文件反编译为 ASM 代码。

完整代码

ASM关键接口

ClassVisitor

ClassVisitor,在 ASM3.0 中是一个接口,到了 ASM4.0 与 ClassAdapter 抽象类合并。主要负责 “拜访” 类成员信息。其中包括(标记在类上的注解,类的构造方法,类的字段,类的方法,静态代码块),它的完整接口如下:

image-20200509160013663

visit(int , int , String , String , String , String[])

该方法是当扫描类时第一个拜访的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口**)。例如:

1
2
3
4
5
public class TestBean {

等价于:
visit(V1_6, ACC_PUBLIC | ACC_SUPER , "org/more/test/asm/simple/TestBean",
null, "java/lang/Object", null)

第一个参数:表示类版本:V1_6,表示 “.class” 文件的版本是 JDK 1.6。可用的其他版本有:V1_1(JRE_1.1)、V1_2(J2SE_1.2)、V1_3(J2SE_1.3)、V1_4(J2SE_1.4)、V1_5(J2SE_1.5)、V1_6(JavaSE_1.6)、V1_7(JavaSE_1.7)。我们所指的 JDK 6 或 JDK 7 实际上就是只 JDK 1.6 或 JDK 1.7。

第二个参数:表示类的修饰符:修饰符在 ASM 中是以 “ACC_” 开头的常量进行定义。可以作用到类级别上的修饰符有:ACC_PUBLIC(public)、ACC_PRIVATE(private)、ACC_PROTECTED(protected)、ACC_FINAL(final)、ACC_SUPER(extends)、ACC_INTERFACE(接口)、ACC_ABSTRACT(抽象类)、ACC_ANNOTATION(注解类型)、ACC_ENUM(枚举类型)、ACC_DEPRECATED(标记了@Deprecated注解的类)、ACC_SYNTHETIC。

第三个参数:表示类的名称:通常我们的类完整类名使用 “org.test.mypackage.MyClass” 来表示,但是到了字节码中会以路径形式表示它们 “org/test/mypackage/MyClass” 值得注意的是虽然是路径表示法但是不需要写明类的 “.class” 扩展名。

第四个参数:表示泛型信息,如果类并未定义任何泛型该参数为空。Java 字节码中表示泛型时分别对接口和类采取不同的定义。该参数的内容格式如下:

1
2
3
<泛型名:基于的类型....>Ljava/lang/Object;

<泛型名::基于的接口....>Ljava/lang/Object;

其中 “泛型名:基于的类型” 内容可以无限的写下去,例如:

1
2
3
4
5
6
7
8
9
10
public class TestBean<T,V,Z> {

泛型参数为:<T:Ljava/lang/Object;V:Ljava/lang/Object;Z:Ljava/lang/Object;>Ljava/lang/Object;
分析结构如下:
<
T:Ljava/lang/Object;
V:Ljava/lang/Object;
Z:Ljava/lang/Object;
>
Ljava/lang/Object;

再或者:

1
2
3
4
5
6
7
8
9
public class TestBean<T extends Date, V extends ArrayList> {

泛型参数为:<T:Ljava/util/Date;V:Ljava/util/ArrayList;>Ljava/lang/Object;
分析结构如下:
<
T:Ljava/util/Date;
V:Ljava/util/ArrayList;
>
Ljava/lang/Object;

以上内容只是针对泛型内容是基于某个具体类型的情况,如果泛型是基于接口而非类型则定义方式会有所不同,这一点需要注意。例如:

1
2
3
4
5
6
7
8
9
public class TestBean<T extends Serializable, V> {

泛型参数为:<T::Ljava/io/Serializable;V:Ljava/lang/Object;>Ljava/lang/Object;
分析结构如下:
<
T::Ljava/io/Serializable; //比类型多出一个“:”
V:Ljava/lang/Object;
>
Ljava/lang/Object;

第五个参数:表示所继承的父类。由于 Java 的类是单根结构,即所有类都继承自 java.lang.Object 因此可以简单的理解为任何类都会具有一个父类。虽然在编写 Java 程序时我们没有去写 extends 关键字去明确继承的父类,但是 JDK在编译时 总会为我们加上 “ extends Object”。所以倘若某一天你看到这样一份代码也不要过于紧张。

第六个参数:表示类实现的接口,在 Java 中类是可以实现多个不同的接口因此此处是一个数组例如:

1
2
3
public class TestBean implements Serializable , List {

该参数会以 “[java/io/Serializable, java/util/List]” 形式出现。

这里需要补充一些内容,如果类型其本身就是接口类型。对于该方法而言,接口的父类类型是 “java/lang/Object”,接口所继承的所有接口都会出现在第六个参数中。例如:

1
2
3
4
public inteface TestBean implements Serializable , List {

最后两个参数对应为:
"java/lang/Object", ["java/io/Serializable","java/util/List"]

visitAnnotation(String , boolean)

该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在 JVM 中可见)。例如:

1
2
3
4
5
@Bean({ "" })
public class TestBean {

@Bean等价于:
visitAnnotation("Lnet/hasor/core/gift/bean/Bean;", true);

下面是 @Bean 的源代码:

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface Bean {
/** Bean名称。*/
public String[] value();
}
1
2
3
4
5
6
7
8
9
10
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
if (cv != null) {
return cv.visitAnnotation(descriptor, visible);
}
return null;
}

descriptor:表示类注解类的描述;
visible表示该注解是否运行时可见;
return AnnotationVisitor:表示该注解类的Visitor,可以用来访问注解值;

第一个参数:表示的是,注解的类型。它使用的是(“L” + “类型路径” + “;”)形式表述。

第二个参数:表示的是,该注解是否在 JVM 中可见。这个参数的具体含义可以理解为:如果为 true 表示虚拟机可见,我们可以通过下面这样的代码获取到注解类型:

1
testBeanType.getAnnotation(TestAnno.class);

谈到这里就需要额外说明一下在声明注解时常见的 “@Retention(RetentionPolicy.RUNTIME)” 标记。RetentionPolicy 是一个枚举它具备三个枚举元素其每个含义可以理解为:

  1. RetentionPolicy.SOURCE:声明注解只保留在 Java 源程序中,在编译 Java 类时注解信息不会被写入到 Class。如果使用的是这个配置 ASM 也将无法探测到这个注解。
  2. RetentionPolicy.CLASS:声明注解仅保留在 Class 文件中,JVM 运行时并不会处理它,这意味着 ASM 可以在 visitAnnotation 时候探测到它,但是通过Class 反射无法获取到注解信息。
  3. RetentionPolicy.RUNTIME:这是最常用的一种声明,ASM 可以探测到这个注解,同时 Java 反射也可以取得注解的信息。所有用到反射获取的注解都会用到这个配置,就是这个原因。

visitField(int , String , String , String , Object)

该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestBean {
private String stringData;

stringData字段等价于:
visitField(ACC_PRIVATE, "stringData", "Ljava/lang/String;", null, null)

public FieldVisitor visitField( final int access, final String name, final String descriptor,final String signature,
final Object value) {
if (cv != null) {
return cv.visitField(access, name, descriptor, signature, value);
}
return null;
}

其中
access:表示该域的访问方式,publicprivate或者static,final等等;
name:指的是域的名称;
descriptor:域的描述,一般指的是该field的参数类型;
signature:指的是域的签名,一般是泛型域才会有签名;
value:指的该域的初始值
reture FiedVisitor:表示将返回一个可以访问该域注解和属性的访问对象,如果不感兴趣的话,可以设置为空;

access:表示字段的修饰符,修饰符在 ASM 中是以 “ACC_” 开头的常量进行定义。可以作用到字段级别上的修饰符有:ACC_PUBLIC(public)、ACC_PRIVATE(private)、ACC_PROTECTED(protected)、ACC_STATIC(static)、ACC_FINAL(final)、ACC_VOLATILE(volatile)、ACC_TRANSIENT(transient)、ACC_ENUM(枚举)、ACC_DEPRECATED(标记了@Deprecated注解的字段)、ACC_SYNTHETIC。

name:表示field的名称。

descriptor:表示field的参数类型,类的描述,其格式为:(“L” + 类型路径 + “;”)。

signature:表示泛型信息, 泛型类型描述是使用(“T” + 泛型名 + “;”)加以说明。例如:“private T data;” 字段的泛型描述将会是 “ TT; ”, “ private V data; ” 字段的泛型描述将会是 “ TV; ”。例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class TestBean<T, V> {
private T data;
private V value;

等价于:

visit(V1_6, ACC_PUBLIC | ACC_SUPER , "org/more/test/asm/simple/TestBean",
"<T:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/lang/Object;", //定义了两个泛型类型 T 和 V
"java/lang/Object", null)

visitField(ACC_PRIVATE, "data", "Ljava/lang/Object;", "TT;", null) //data 泛型名称为 T
visitField(ACC_PRIVATE, "value", "Ljava/lang/Object;", "TV;", null) // value 泛型名称为 V

还有一种情况,倘若类在定义泛型时候已经基于某个类型那么生成的代码将会是如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
public class TestBean<T extends Serializable, V> {
private T data;
private V value;

等价于:

visit(V1_6, ACC_PUBLIC | ACC_SUPER , "org/more/test/asm/simple/TestBean",
"<T::Ljava/io/Serializable;V:Ljava/lang/Object;>Ljava/lang/Object;", //定义了两个泛型类型 T 和 V
"java/lang/Object", null)

visitField(ACC_PRIVATE, "data", "Ljava/io/Serializable;", "TT;", null) //data 泛型名称为 T
visitField(ACC_PRIVATE, "value", "Ljava/lang/Object;", "TV;", null) // value 泛型名称为 V

value:表示的是默认值, 由于默认值是 Object 类型大家可能以为可以是任何类型。这里要澄清一下,默认值中只能用来表述 Java 基本类型这其中包括了(byte、sort、int、long、float、double、boolean、String)其他所有类型都不可以进行表述。并且只有标有 “final” 修饰符的字段并且该字段赋有初值时这个参数才会有值。例如类:

1
2
3
4
5
6
public class TestBean {
private final String data;
public TestBean() {
data = "aa";
}
....

在执行 “visitField” 方法时候,这个参数的就是 null 值,下面这种代码也会是 null 值:

1
2
3
4
5
6
public class TestBean {
private final Date data;
public TestBean() {
data =new Date();
}
....

此外如果字段使用的是基本类型的包装类型,诸如:Integer、Long…也会为空值:

1
2
3
public class TestBean {
private final Integer intData = 12;
...

能够正确得到默认值的代码应该是这个样子的:

1
2
3
4
public class TestBean {
private final String data = "ABC";
private final int intData = 12;
...

visitMethod(int , String , String , String , String[])

该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestBean {
public int halloAop(String param) throws Throwable {

等价于:

visit(V1_6, ACC_PUBLIC | ACC_SUPER , "org/more/test/asm/simple/TestBean",
null, "java/lang/Object", null)

visitMethod(ACC_PUBLIC, "<init>", "()V", null, null)
visitMethod(ACC_PUBLIC, "halloAop", "(Ljava/lang/String;)I", null, [java/lang/Throwable])


public MethodVisitor visitMethod( final int access,final String name,final String descriptor,final String signature, final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}

access:表示方法的修饰符,修饰符在 ASM 中是以 “ACC_” 开头的常量进行定义。可以作用到方法级别上的修饰符有:ACC_PUBLIC(public)、ACC_PRIVATE(private)、ACC_PROTECTED(protected)、ACC_STATIC(static)、ACC_FINAL(final)、ACC_SYNCHRONIZED(同步的)、ACC_VARARGS(不定参数个数的方法)、ACC_NATIVE(native类型方法)、ACC_ABSTRACT(抽象的)、ACC_DEPRECATED(标记了@Deprecated注解的方法)、ACC_STRICT、ACC_SYNTHETIC。

name:表示方法名,在 ASM 中 “visitMethod” 方法会处理(构造方法、静态代码块、私有方法、受保护的方法、共有方法、native类型方法)。在这些范畴中构造方法的方法名为 “”,静态代码块的方法名为 “”。列如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestBean {
public int halloAop(String param) throws Throwable {
return 0;
}
static {
System.out.println();
}
...

等价于:

visit(V1_6, ACC_PUBLIC | ACC_SUPER , "org/more/test/asm/simple/TestBean",
null, "java/lang/Object", null)

visitMethod(ACC_PUBLIC, "<clinit>", "()V", null, null)
visitMethod(ACC_PUBLIC, "<init>", "()V", null, null)
visitMethod(ACC_PUBLIC, "halloAop", "(Ljava/lang/String;)I", null, [java/lang/Throwable])

descriptor:表示方法签名,方法签名的格式如下:“(参数列表)返回值类型”。在字节码中不同的类型都有其对应的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
"I"        = int
"B" = byte
"C" = char
"D" = double
"F" = float
"J" = long
"S" = short
"Z" = boolean
"V" = void
"[...;" = 数组
"[[...;" = 二维数组
"[[[...;" = 三维数组
"L....;" = 引用类型

下面是一些方法签名对应的方法参数列表:

image-20200509162013272

signature:凡是具有泛型信息的方法,该参数都会有值。并且该值的内容信息基本等于第三个参数的拷贝,只不过不同的是泛型参数被特殊标记出来。例如:

1
2
3
4
5
public class TestBean<T, V extends List> {
public T halloAop(V abc, int aaa) throws Throwable {

方法签名:(Ljava/util/List;I)Ljava/lang/Object;
泛型签名:(TV;I)TT;
1
2
3
4
5
public class TestBean<T, V extends List> {
public String halloAop(V abc, int aaa) throws Throwable {

方法签名:(Ljava/util/List;I)Ljava/lang/String;
泛型签名:(TV;I)Ljava/lang/String;

可以看出泛型信息中用于标识泛型类型的结构是(“T” + 泛型名 + “;”),还有一种情况就是。泛型是声明在方法上。例如:

1
2
3
4
5
public class TestBean {
public <T extends List> String halloAop(T abc, int aaa) throws Throwable {

方法签名:(Ljava/util/List;I)Ljava/lang/String;
泛型签名:<T::Ljava/util/List;>(TT;I)Ljava/lang/String; //泛型类型基于接口
1
2
3
4
5
public class TestBean {
public <T> String halloAop(T abc, int aaa) throws Throwable {

方法签名:(Ljava/lang/Object;I)Ljava/lang/String;
泛型签名:<T:Ljava/lang/Object;>(TT;I)Ljava/lang/String; //泛型类型基于类型

exceptions:用来表示将会抛出的异常,如果方法不会抛出异常,则该参数为空。这个参数的表述形式比较简单,举一个例子:

1
2
3
4
public class TestBean {
public <T> String halloAop(T abc, int aaa) throws Throwable,Exception {

异常参数为:[java/lang/Throwable, java/lang/Exception]

MethodVisitor

假设有下面这样的一个类:

1
2
3
4
5
public class DemoClass {
public static void main(String[] args) {
System.out.println();
}
}

通过Javap可以得到下面这样的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ javap -c classtest.DemoClass
Compiled from "DemoClass.java"
public class classtest.DemoClass extends java.lang.Object{
public classtest.DemoClass();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: getstatic #16; //Field java/lang/System.out:Ljava/io/PrintStream;
3: invokevirtual #22; //Method java/io/PrintStream.println:()V
6: return

}

可以看出其实Java编译完这个类之后是产生了两个方法。其中一个是第四行表示的“public classtest.DemoClass();”它是构造方法。

和第十行表示的“main”方法。下面这段例子用来扫描这个类的这两个方法,我们的扫描逻辑很简单就是当遇到一个定义的方法时输出这个方法名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class DemoClassTest {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader(DemoClass.class.getName());
cr.accept(new DemoClassVisitor(), ClassReader.SKIP_DEBUG);
System.out.println("---ALL END---");
}
}
class DemoClassVisitor extends ClassVisitor {
public DemoClassVisitor() {
super(Opcodes.ASM4);
}
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println("at Method " + name);
//
MethodVisitor superMV = super.visitMethod(access, name, desc, signature, exceptions);
return new DemoMethodVisitor(superMV, name);
}
}
class DemoMethodVisitor extends MethodVisitor {
private String methodName;
public DemoMethodVisitor(MethodVisitor mv, String methodName) {
super(Opcodes.ASM4, mv);
this.methodName = methodName;
}
public void visitCode() {
System.out.println("at Method ‘" + methodName + "’ Begin...");
super.visitCode();
}
public void visitEnd() {
System.out.println("at Method ‘" + methodName + "’End.");
super.visitEnd();
}
}
  1. 上面这段程序首先在第三行使用 ClassReader 去读取 DemoClass 类的字节码信息。
  2. 其次通过“cr.accept(new DemoClassVisitor(), ClassReader.SKIP_DEBUG);”方法开始Visitor扫描整个字节码。
  3. SKIP_DEBUG选项的意义是在扫描过程中掠过所有有关行号方面的内容。
  4. 在DemoClassVisitor类中我们重写了visitMethod方法,当遇到方法的时候打印出方法名。
  5. 随后我们返回DemoMethodVisitor对象,用以输出方法的开始和结束。

上面这段程序的输出如下:

1
2
3
4
5
6
7
at Method <init>
at Method ‘<init>’ Begin...
at Method ‘<init>’End.
at Method main
at Method ‘main’ Begin...
at Method ‘main’End.
---ALL END---

下面是这个MethodVisitor接口的所有方法定义:

image-20200509163546690

虽然该接口的方法数量如此之多,甚至是ClassVisitor接口的3倍以上。但是值得我们关心的接口只有下面这几个,其余的都是和代码有关系:

1
2
3
MethodVisitor.visitCode();
MethodVisitor.visitMaxs(maxStack, maxLocals);
MethodVisitor.visitEnd();
  • 第一个方法:表示ASM开始扫描这个方法。
  • 第二个方法:该方法是visitEnd之前调用的方法,可以反复调用。用以确定类方法在执行时候的堆栈大小。
  • 第三个方法:表示方法输出完毕。

构造方法

关于方法名或许读者注意到了在扫描这个类的时候,有一个特殊的方法被扫描到了“”,这个方法就是传说中的构造方法。当Java在编译的时候没有发现类文件中有构造方法的定义会为其创建一个默认的无参构造方法。这个“”就是那个由系统添加的构造方法。现在我们为类填写一个构造方法如下:

1
2
3
4
5
6
7
8
public class DemoClass {
public DemoClass(int a) {

}
public static void main(String[] args) {
System.out.println();
}
}

再次扫描这个类,你会发现它的结果和刚才是一样的,这是由于我们编写的构造方法替换了系统默认生成的那个。

静态代码块

在Class我们接触过用“static { }”包含的代码,这个是我们常说的静态代码块。这个代码快ASM在扫描字节码的时候也会遇到它,别以为这真的是一个什么代码块。所有的静态代码快最后都会放到“”方法中。

静态代码快只有一个,现有下面这个的一个类。在编写这个类的时候我有意的写了两个不同的静态代码块的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DemoClass {
static {
int a;
System.out.println(11);
}
public static void main(String[] args) {
System.out.println();
}
static {
int a;
System.out.println(22);
}
}

ASM在扫描这个类的时候你会发现虽然类中存在多个静态代码快,但是最后类文件中只会出现了一个“”方法。JVM在编译Class的时候估计已经将多个静态代码块合并到一起了。

image-20200509165603790

Access

PUBLIC: 1
PRIVATE: 2
PROTECTED: 4
STATIC: 8
FINAL: 16
SYNCHRONIZED: 32
VOLATILE: 64
TRANSIENT: 128
NATIVE: 256
INTERFACE: 512
ABSTRACT: 1024
STRICT: 2048

Modifier提供了很多静态方法。如public static String toString(int mod)就可以输出该整数对应的所有的修饰符。public static boolean isPublic(int mod)就可以判断该整数对应的是不是包含public修饰符。