记录生活中的点点滴滴

0%

Java的ASM

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

class字节码

我们编写的 Java文件,会通过 javac 命令编译为 class 文件,JVM 最终会执行该类型文件来运行程序。

下面我们通过一个简单的Java例子进行说明,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.gs.asm;

public class Test {
private int num1 = 1;
public static int NUM1 = 100;
public int func(int a,int b){
return add(a,b);
}
public int add(int a,int b) {
return a+b+num1;
}
public int sub(int a, int b) {
return a-b-NUM1;
}
}

我们命令行运行 javac -g Test.java 把这个类编译成 class 文件,然后通过 javap -verbose Test.class 命令查看class文件格式,如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
Classfile /D:/桌面/Java/IdeaProjectes/JavaCoreTechnology2/src/cn/gs/asm/Test.class
Last modified 2020年11月20日; size 672 bytes
MD5 checksum 75564e825d080aedb8cbc0ce95212437
Compiled from "Test.java"
public class cn.gs.asm.Test
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // cn/gs/asm/Test
super_class: #6 // java/lang/Object
interfaces: 0, fields: 2, methods: 5, attributes: 1
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#27 // cn/gs/asm/Test.num1:I
#3 = Methodref #5.#28 // cn/gs/asm/Test.add:(II)I
#4 = Fieldref #5.#29 // cn/gs/asm/Test.NUM1:I
#5 = Class #30 // cn/gs/asm/Test
#6 = Class #31 // java/lang/Object
#7 = Utf8 num1
#8 = Utf8 I
#9 = Utf8 NUM1
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcn/gs/asm/Test;
#17 = Utf8 func
#18 = Utf8 (II)I
#19 = Utf8 a
#20 = Utf8 b
#21 = Utf8 add
#22 = Utf8 sub
#23 = Utf8 <clinit>
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #10:#11 // "<init>":()V
#27 = NameAndType #7:#8 // num1:I
#28 = NameAndType #21:#18 // add:(II)I
#29 = NameAndType #9:#8 // NUM1:I
#30 = Utf8 cn/gs/asm/Test
#31 = Utf8 java/lang/Object
{
public static int NUM1;
descriptor: I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC

public cn.gs.asm.Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num1:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcn/gs/asm/Test;

public int func(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: iload_1
2: iload_2
3: invokevirtual #3 // Method add:(II)I
6: ireturn
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcn/gs/asm/Test;
0 7 1 a I
0 7 2 b I

public int add(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: aload_0
4: getfield #2 // Field num1:I
7: iadd
8: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcn/gs/asm/Test;
0 9 1 a I
0 9 2 b I

public int sub(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM1:I
6: isub
7: ireturn
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcn/gs/asm/Test;
0 8 1 a I
0 8 2 b I

static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #4 // Field NUM1:I
5: return
LineNumberTable:
line 5: 0
}
SourceFile: "Test.java"

可以看出在编译为class文件后,字段名称,方法名称,类型名称等均在常量池中存在的。从而做到减小文件的目的。同时方法定义也转变为了jvm指令。下面我们需要对jvm指令加深一下了解。在了解之前需要我们理解JVM基于栈的设计模式。

JVM基于栈的设计模式

JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性。在线程中执行一个方法时,我们会创建一个栈帧入栈并执行,如果该方法又调用另一个方法时会再次创建新的栈帧然后入栈,方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧,随后虚拟机将会丢弃此栈帧。

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。虚拟机通过索引定位的方法查找相应的局部变量。举个例子。以上述的代码为例

1
2
3
public int sub(int a, int b) {
return a-b-NUM1;
}

这个方法大家可以猜测一下局部变量有哪些? 答案是3个,不应该只有a,b吗?还有this,对应实例对象方法编译器都会追加一个this参数。如果该方法为静态方法则为2个了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int sub(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM1:I
6: isub
7: ireturn
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/wuba/asmdemo/Test;
0 8 1 a I
0 8 2 b I

所以局部变量表第0个元素为this, 第一个为a,第二个为b

操作数栈

通过局部变量表我们有了要操作和待更新的数据,我们如果对局部变量这些数据进行操作呢?通过操作数栈。当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

JVM指令

  • load 命令:用于将局部变量表的指定位置的相应类型变量加载到操作数栈顶;
  • store命令:用于将操作数栈顶的相应类型数据保入局部变量表的指定位置;
  • invokevirtual:调用实例方法
  • ireturn:当前方法返回int

ASM

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

使用ASM框架需要导入asm的jar包,下载链接:asm-3.2.jar

ASM框架中的核心类有以下几个:

  ① ClassReader:该类用来解析编译过的class字节码文件。

  ② ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。

  ③ ClassAdapter:该类也实现了ClassVisitor接口,它将对它的方法调用委托给另一个ClassVisitor对象。

实例1:通过ASM生成类的字节码

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
//通过asm生成类的字节码
public class GeneratorClass {
public static void main(String[] args) throws IOException {
//生成一个类只需要ClassWriter组件即可
ClassWriter cw = new ClassWriter(0);
//通过visit方法确定类的头部信息
cw.visit(Opcodes.V1_5,Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE,
"cn/gs/asm/Comparable",null,"java/lang/Object",new String[]{"cn/gs/asm/Mesurable"});
//定义类的属性
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"LESS","I",null,new Integer(-1)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"EQUAL","I",null,new Integer(0)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"GREATER","I",null,new Integer(1)).visitEnd();
//定义类的方法
cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT,
"compareTo","(Ljava/lang/Object;)I",null,null).visitEnd();
//使cw类已经完成
cw.visitEnd();
//将cw转换成字节数组写到文件里面去
byte[] data = cw.toByteArray();
File file = new File("D://Comparable.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
}
}

执行main方法之后会在D盘下生成一个Comparable的class文件,我们可以通过命令 javap -c Comparable.class >test.txt 去反编译这个class文件生成 Java 代码,如下:

1
2
3
4
5
6
7
8
9
public interface cn.gs.asm.Comparable extends cn.gs.asm.Mesurable {
public static final int LESS;

public static final int EQUAL;

public static final int GREATER;

public abstract int compareTo(java.lang.Object);
}

注:一个编译后的java类不包含package和import段,因此在class文件中所有的类型都使用的是全路径。

ASM还可以修改类的字节码文件,就不写了,俺看不懂!应该还是功夫还不到家吧,就这样吧。