字节码&ASM-基础
在接触ASM之前不得不去了解一些字节码相关的前置知识,掌握字节码的类型描述、JVM代码执行模型是开启ASM大门的钥匙,掌握这把钥匙开启大门才能走上一条康庄大道通往ASM,否则一路摸爬滚打也许就中途放弃了。
字节码中的类型描述
字节码中类型描述其实就是JAVA中类型对应的描述,即JAVA中基础类型和对象,基础类型大部分采用基础类型首字母,但个别因首字母被赋予了其他特殊含义便使用另外的一个字母表示,而对象采用统一的规则进行表示。
字节码类型描述
JAVA类型 | 类型描述 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
void | V |
Object | Ljava/lang/Object; |
Object[] | [Ljava/lang/Object; |
Object[][] | [[Ljava/lang/Object; |
Object[][][]... | [[[...Ljava/lang/Object; |
基础类型描述符不在过多赘述,而对象无疑看起来率微复杂些,类描述规则:L + 类全限定名 + ;以大写L标志开始;为结束标志
,中间内容为类全限定名,类全限定名规则:包名以/分隔符 + 类名
,例如Object
类全限定名为java/lang/Object
,类描述为Ljava/lang/Obhect;
。数组以[ + 数组元素类型描述
表示,一维数组[
,二维数组[[
,三维数组[[[
...以此类推
字节码方法描述
方法描述在字节码中规则:(所有参数类型描述)返回值类型描述
,当然其中有两个特殊的需要注意,方法名分别为<init>
、<client>
,其中<init>
代表构造函数,<client>
静态代码块。例如方法void makeName(String firstName, String lastName)
的方法描述为(Ljava/lang/String;Ljava/lang/String;)
字节码
众所周知JAVA是一种跨平台语言,JVM为JAVA在不同平台表现一致性,需要注意的是JVM并不是跨平台的,不同平台的JVM帮住JAVA程序员屏蔽了平台间的差异。JVM通过加载和执行同一种于平台无关的字节码,而字节码由不同语言通过编译后得到的产物
JVM指令由一个字节的操作码(opcode
)和紧随其后的可选操作数(operand
)构成,比如: opcode operand1, operand2
而JVM执行指令的过程其实就是不断将opcode
和operand
出栈和入栈的过程,比如a = 1
就是通过将变量a
压入执行栈后再将1压入执行栈后再通过存储指令将a
进行存储
字节码名字的由来是因为操作码的大小为一个字节,所以操作码集最多由256个,而目前已经使用了超过200个了,虽然操作码的数量已经相当的庞大,但是其中有着很多类似的操作码,这是因为操作码大多是于类型有关的,比如对变量存储的操作码就有ISTORE
、LSTORE
、FSTORE
、DSTORE
、ASTORE
、IASTORE
、LASTORE
、FASTORE
、DASTORE
、AASTORE
、BASTORE
、CASTORE
、SASTORE
这13个操作码,不难发现其中一般为类型描述符 + STORE
JAVA虚拟机栈和栈帧
虚拟机实现方式常见的有两种:基于栈和基于寄存器。基于栈的虚拟机有大名鼎鼎的Hotspot JVM
,基于寄存器的虚拟机有Lua
的Lua VM
和Google的Android虚拟机DalvikVM
二者有什么不同呢?举一个非常简单的例子:c = a + b
int add (int a, int b) {
return a + b;
}
JAVA:
0: iload_1
1: iload_2
2: iadd
3: ireturn
第一行指令加a压入栈顶,为什么是iload_1
而不是iload_0
呢?这是因为非类非静态方法编译后在方法局部变量表中第0索引插入了类实例的指针,所以比如类似Golong在书写结构体方法是第一个需要传入结构体指针,而JAVA并非不用穿只是编译器帮你做了而已,好了有些跑偏了,回归正轨,第二行将b压入栈顶,第三行将操作栈中a、b出栈求和后入栈,第四行将上一步求和压入栈顶的值返回,方法结束。
LUA:
[1] ADD R2 R0 R1
[2] RETURN R2 2
[3] RETURN R0 1
第一行调用ADD
指令将R0
和R1
中的值求和存储到R2
,第二行返回R2
的值,第三行则为lua
特殊的处理为了防止分支遗漏return
指令
基于栈和基于寄存器架构优缺点:
- 基于栈移植性好、指令更短、实现简单,但不能随机访问堆栈中元素,完成相同功能比基于寄存器所需要的指令数一般都要多,需要频繁出入栈
- 基于寄存器速度快,可以充分利用寄存器,操作数需要显示指定,指令较长
帧栈
Hotspot JVM是一款基于栈的虚拟机,每个线程都有一个虚拟机栈用来存储栈帧,每次方法的调用都将伴随着栈帧的创建、销毁。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧的存储控件分配在JAVA虚拟机栈中,每个栈帧都拥有自己的局部变量表(Local Variable)、操作数栈、常量池的引用。
package com.river.asm;
public class HelloWorld {
private String word = "hello world";
public void say() {
System.out.println(word);
}
public static void main(String[] args) {
new HelloWorld().say();
}
}
通过javap -v HelloWorld
输出字节码相关信息,这里给出say
方法相关内容
public void say();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #3 // Field word:Ljava/lang/String;
7: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 7: 0
line 8: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/river/asm/HelloWorld;
局部变量表
每个栈帧内部都包含一组称为局部变量表的变量列表,局部变量表长度在编译期间确定,对应ClassFile
中的max_locals
,JVM根据max_locals
字段来分配方法执行过程所需要的最大容量,需要注意的是long和double需要占用两个变量槽
操作数栈
每个栈帧内部包含一个称为操作数栈的后进先出栈,大小同样在编译期间确定。JVM提供很多字节码指令用于从局部变量表或者对象实例的字段数值压入到操作数栈中,也有一些指定将操作数栈中的值弹出进行一些计算后在复制到局部变量表或类实力字段中去。调用方法时,操作数栈也需要准备好调用方法的参数和接受方法返回的结果。JVM执行指令的过程就是不断的入栈和出栈达到修改局部变量表或类字段的过程。而在方法执行过程中操作数栈的最大值就是ClassFile
中的max_stack
存储操作符
存储的操作就是将栈顶元素存入到指定索引的变量表中
操作符名称 | 值 | 含义 |
---|---|---|
ISTORE | 54 | 栈顶 int 型数值存入指定局部变量 |
LSTORE | 55 | 栈顶 long 型数值存入指定局部变量 |
FSTORE | 56 | 栈顶 float 型数值存入指定局部变量 |
DSTORE | 57 | 栈顶 double 型数值存入指定局部变量 |
ASTORE | 58 | 栈顶 object 型数值存入指定局部变量 |
IASTORE | 79 | 栈顶 int 型数值存入数组指定索引位置 |
LASTORE | 80 | 栈顶 long 型数值存入数组指定索引位置 |
FASTORE | 81 | 栈顶 float 型数值存入数组指定索引位置 |
DASTORE | 82 | 栈顶 double 型数值存入数组指定索引位置 |
AASTORE | 83 | 栈顶 object 型数值存入数组指定索引位置 |
BASTORE | 84 | 栈顶 byte | boolean 型数值存入数组指定索引位置 |
CASTORE | 85 | 栈顶 char 型数值存入数组指定索引位置 |
SASTORE | 86 | 栈顶 short 型数值存入数组指定索引位置 |
加载操作符
加载的操作就是将变量表中指定索引的值压入到栈顶
操作符名称 | 值 | 含义 |
---|---|---|
ILOAD | 21 | 局部变量表指定索引类型为 int 型数值压入栈顶 |
LLOAD | 22 | 局部变量表指定索引类型为 long 型数值压入栈顶 |
FLOAD | 23 | 局部变量表指定索引类型为 float 型数值压入栈顶 |
DLOAD | 24 | 局部变量表指定索引类型为 double 型数值压入栈顶 |
ALOAD | 25 | 局部变量表指定索引类型为 object 型数值压入栈顶 |
IALOAD | 46 | 局部变量表指定索引类型为 int[] 的指定索引的值压入栈顶 |
LALOAD | 47 | 局部变量表指定索引类型为 long[] 的指定索引的值压入栈顶 |
FALOAD | 48 | 局部变量表指定索引类型为 float[] 的指定索引的值压入栈顶 |
DALOAD | 49 | 局部变量表指定索引类型为 double[] 的指定索引的值压入栈顶 |
AALOAD | 50 | 局部变量表指定索引类型为 object[] 的指定索引的值压入栈顶 |
BALOAD | 51 | 局部变量表指定索引类型为 byte | boolean[] 的指定索引的值压入栈顶 |
CALOAD | 52 | 局部变量表指定索引类型为 char[] 的指定索引的值压入栈顶 |
SALOAD | 53 | 局部变量表指定索引类型为 short[] 的指定索引的值压入栈顶 |
常量类型操作符
常量类型操作符其实和加载一样,也是将某个值压入栈顶,记得上一篇文章学习ClassFile中提到一些变量存储在常量池中,一些以字节码形式内嵌到了CODE_ATTR里面,而内嵌到CODE_ATTR中的就是下面的操作符
操作符名称 | 值 | 含义 |
---|---|---|
ACONST_NULL | 1 | 将null压入栈顶 |
ICONST_M1 | 2 | 将-1压入栈顶 |
ICONST_0 | 3 | 将0压入栈顶 |
ICONST_1 | 4 | 将1压入栈顶 |
ICONST_2 | 5 | 将2压入栈顶 |
ICONST_3 | 6 | 将3压入栈顶 |
ICONST_4 | 7 | 将4压入栈顶 |
ICONST_5 | 8 | 将5压入栈顶 |
LCONST_0 | 9 | 将0L压入栈顶 |
LCONST_1 | 10 | 将1L压入栈顶 |
FCONST_0 | 11 | 将0F压入栈顶 |
FCONST_1 | 12 | 将1F压入栈顶 |
FCONST_2 | 13 | 将2F压入栈顶 |
DCONST_0 | 14 | 将0F压入栈顶 |
DCONST_1 | 15 | 将1F压入栈顶 |
BIPUSH | 16 | 将单字节常量(-128~127)压入栈顶 |
SIPUSH | 17 | 将short 常量(-32768~32767)压入栈顶 |
LDC | 18 | 将常量池中的 int,float,String 型常量取出并压入栈顶 |
栈操作
操作符名称 | 值 | 含义 |
---|---|---|
POP | 87 | 将栈顶值弹出 |
POP2 | 88 | 将栈顶的一个 long 或 double 值弹出,或弹出 2 个其他类型数值 |
DUP | 89 | 复制栈顶值并压入栈顶 |
DUP_X1 | 90 | 复制栈顶数值并将两个复制值压入栈顶 |
DUP_X2 | 91 | 复制栈顶数值并将三个或两个复制值压入栈顶 |
DUP2 | 92 | 复制栈顶一个(long 或 double 类型的) 或两个(其它)数值并将复制值压入栈顶 |
DUP2_X1 | 93 | 参考DUP_X1 |
DUP2_X2 | 94 | 参考DUP_X2 |
SWAP | 95 | 交换栈顶连个值 |
这里需要注意到的是long和double占用两个变量索引位置,所以尽量就使用POP2
、DUP2
...
运算操作符
操作符名称 | 值 | 含义 |
---|---|---|
IADD | 96 | 将栈顶两个int值相加后压入栈顶 |
LADD | 97 | 将栈顶两个long值相加后压入栈顶 |
FADD | 98 | 将栈顶两个float值相加后压入栈顶 |
DADD | 99 | 将栈顶两个double值相加后压入栈顶 |
ISUB | 100 | 将栈顶两个int值求差后压入栈顶 |
LSUB | 101 | 将栈顶两个long值求差后压入栈顶 |
FSUB | 102 | 将栈顶两个float值求差后压入栈顶 |
DSUB | 103 | 将栈顶两个double值求差后压入栈顶 |
IMUL | 104 | 将栈顶两个int值求积后压入栈顶 |
LMUL | 105 | 将栈顶两个long值求积后压入栈顶 |
FMUL | 106 | 将栈顶两个float值求积后压入栈顶 |
DMUL | 107 | 将栈顶两个double值求积后压入栈顶 |
IDIV | 108 | 将栈顶两个int值求商后压入栈顶 |
LDIV | 109 | 将栈顶两个long值求商后压入栈顶 |
FDIV | 110 | 将栈顶两个float值求商后压入栈顶 |
DDIV | 111 | 将栈顶两个double值求商后压入栈顶 |
IREM | 112 | 将栈顶两个int值求余后压入栈顶 |
LREM | 113 | 将栈顶两个long值求余后压入栈顶 |
FREM | 114 | 将栈顶两个float值求余后压入栈顶 |
DREM | 115 | 将栈顶两个double值求余后压入栈顶 |
INEG | 116 | 将栈顶int值求反后压入栈顶 |
LNEG | 117 | 将栈顶long值求反后压入栈顶 |
FNEG | 118 | 将栈顶float值求反后压入栈顶 |
DNEG | 119 | 将栈顶double值求反后压入栈顶 |
ISHL | 120 | 将栈顶int值左移指定位数后压入栈顶 |
LSHL | 121 | 将栈顶long值左移指定位数后压入栈顶 |
ISHR | 122 | 将栈顶int值右移指定位数后压入栈顶 |
LSHR | 123 | 将栈顶long值右移指定位数后压入栈顶 |
IUSHR | 124 | 将栈顶int值右移指定位数后压入栈顶,无符号 |
LUSHR | 125 | 将栈顶long值右移指定位数后压入栈顶,无符号 |
IAND | 126 | 将栈顶两个int值按位与运算后压入栈顶 |
LAND | 127 | 将栈顶两个long值按位与运算后压入栈顶 |
IOR | 128 | 将栈顶两个int值按位或运算后压入栈顶 |
LOR | 129 | 将栈顶两个long值按位或运算后压入栈顶 |
IXOR | 130 | 将栈顶两个int值按位异或运算后压入栈顶 |
LXOR | 131 | 将栈顶两个long值按位异或运算后压入栈顶 |
IINC | 132 | 将栈顶int值自增指定值后压入栈顶 |
转换操作符
操作符名称 | 值 | 含义 |
---|---|---|
I2L | 133 | 将栈顶int值强转为long后压入栈顶 |
I2F | 134 | 将栈顶int值强转为float后压入栈顶 |
I2D | 135 | 将栈顶int值强转为double后压入栈顶 |
L2I | 136 | 将栈顶long值强转为int后压入栈顶 |
L2F | 137 | 将栈顶long值强转为float后压入栈顶 |
L2D | 138 | 将栈顶long值强转为double后压入栈顶 |
F2I | 139 | 将栈顶float值强转为int后压入栈顶 |
F2L | 140 | 将栈顶float值强转为long后压入栈顶 |
F2D | 141 | 将栈顶float值强转为double后压入栈顶 |
D2I | 142 | 将栈顶double值强转为int后压入栈顶 |
D2L | 143 | 将栈顶double值强转为long后压入栈顶 |
D2F | 144 | 将栈顶double值强转为float后压入栈顶 |
I2B | 145 | 将栈顶int值强转为byte后压入栈顶 |
I2C | 146 | 将栈顶int值强转为char后压入栈顶 |
I2S | 147 | 将栈顶int值强转为short后压入栈顶 |
比较操作符
操作符名称 | 值 | 含义 |
---|---|---|
LCMP | 148 | 比较栈顶两 long 型数值大小,并将结果(1,0,-1)压入栈顶 |
FCMPL | 149 | 比较栈顶两 float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为 “NaN” 时,将 - 1 压入栈顶 |
FCMPG | 150 | 比较栈顶两 float 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为 “NaN” 时,将 1 压入栈顶 |
DCMPL | 151 | 比较栈顶两 double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为 “NaN” 时,将 - 1 压入栈顶 |
DCMPG | 152 | 比较栈顶两 double 型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为 “NaN” 时,将 1 压入栈顶 |
IFEQ | 153 | 当栈顶 int 型数值等于 0 时,跳转 |
IFNE | 154 | 当栈顶 int 型数值不等于 0 时,跳转 |
IFLT | 155 | 当栈顶 int 型数值小于 0 时,跳转 |
IFGE | 156 | 当栈顶 int 型数值大于等于 0 时,跳转 |
IFGT | 157 | 当栈顶 int 型数值大于 0 时,跳转 |
IFLE | 158 | 当栈顶 int 型数值小于等于 0 时,跳转 |
IF_ICMPEQ | 159 | 比较栈顶两个 int 型数值,等于 0 时,跳转 |
IF_ICMPNE | 160 | 比较栈顶两个 int 型数值,不等于 0 时,跳转 |
IF_ICMPLT | 161 | 比较栈顶两个 int 型数值,小于 0 时,跳转 |
IF_ICMPGE | 162 | 比较栈顶两个 int 型数值,大于等于 0 时,跳转 |
IF_ICMPGT | 163 | 比较栈顶两个 int 型数值,大于 0 时,跳转 |
IF_ICMPLE | 164 | 比较栈顶两个 int 型数值,小于等于 0 时,跳转 |
IF_ACMPEQ | 165 | 比较栈顶两个 object 型数值,相等时跳转 |
IF_ACMPNE | 166 | 比较栈顶两个 object 型数值,不相等时跳转 |
IFNULL | 198 | 为 null 时跳转 |
IFNONNULL | 199 | 非 null 时跳转 |
这里需要指出的是最好记住EQ
、NE
、LT
、GE
、GT
、LE
这几个单词的含义。
跳转控制操作符
操作符名称 | 值 | 含义 |
---|---|---|
GOTO | 167 | 无条件分支跳转 |
JSR | 168 | 跳转至指定16位 offset(bit) 位置,并将 jsr 下一条指令地址压入栈顶 |
RET | 169 | 返回至局部变量指定的 index 的指令位置 |
TABLESWITCH | 170 | 用于 switch 条件跳转,case 值连续 |
LOOKUPSWITCH | 171 | 用于 switch 条件跳转,case 值不连续 |
IRETURN | 172 | 结束方法,并返回一个 int 类型数据 |
LRETURN | 173 | 结束方法,并返回一个 long 类型数据 |
FRETURN | 174 | 结束方法,并返回一个 float 类型数据 |
DRETURN | 175 | 结束方法,并返回一个 double 类型数据 |
ARETURN | 176 | 结束方法,并返回一个 object 类型数据 |
RETURN | 177 | 结束方法,并返回一个 void 类型数据 |
类成员操作符
操作符名称 | 值 | 含义 |
---|---|---|
GETSTATIC | 178 | 类指定静态成员字段压入栈顶 |
PUTSTATIC | 179 | 存储栈顶数据至类指定静态成员字段 |
GETFIELD | 180 | 类成员字段压入栈顶 |
PUTFIELD | 181 | 存储栈顶数据至类指定成员字段 |
方法操作符
操作符名称 | 值 | 含义 |
---|---|---|
INVOKEVIRTUAL | 182 | 调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派) |
INVOKESPECIAL | 183 | 调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |
INVOKESTATIC | 184 | 调用静态方法 |
INVOKEINTERFACE | 185 | 调用接口方法调,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 |
INVOKEDYNAMIC | 186 | 调用动态链接方法(该指令是指令是 Java SE 7 中新加入的)。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面 4 条调用指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的 |
实例化操作符
操作符名称 | 值 | 含义 |
---|---|---|
NEW | 187 | 创建一个对象,并将其引用值压入栈顶 |
NEWARRAY | 188 | 创建一个指定原始类型(如 int、float、char……)的数组,并将其引用值压入栈顶 |
ANEWARRAY | 189 | 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶 |
数组相关操作符
操作符名称 | 值 | 含义 |
---|---|---|
ARRAYLENGTH | 190 | 获得数组的长度值并压入栈顶 |
MULTIANEWARRAY | 197 | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
异常相关操作符
操作符名称 | 值 | 含义 |
---|---|---|
ATHROW | 191 | 将栈顶的异常直接抛出。Java 程序中显式抛出异常的操作(throw 语句)都由 athrow 指令来实现,并且,在 Java 虚拟机中,处理异常(catch 语句)不是由字节码指令来实现的,而是采用异常表来完成的 |
类型相关操作符
操作符名称 | 值 | 含义 |
---|---|---|
CHECKCAST | 192 | 检验类型转换,检验未通过将抛出 ClassCastException |
INSTANCEOF | 193 | 检验对象是否是指定的类的实例,如果是将 1 压入栈顶,否则将 0 压入栈顶 |
同步锁操作符
操作符名称 | 值 | 含义 |
---|---|---|
MONITORENTER | 194 | 获取对象的 monitor,用于同步块或同步方法 |
MONITOREXIT | 195 | 释放对象的 monitor,用于同步块或同步方法 |
这些操作符位于ASM的org.objectweb.asm.Opcodes
中,将操作符按操类型进行分组后非常的清晰,抛去同等意义的操作符其实也就并没有太多了,而每种操作符需要栈顶值的要求这是一个关键,后续会陆陆续续看到相关操作符的实际使用
ASM的启程
操作字节码的框架有很多,但是不论如何我想ASM无疑是绕不开的话题,ASM也许对入口及学习陡度上都不算友好,但是个人认为ASM作为字节码操作框架中于class文件结合的最完美的框架,ASM虽然学习需要比平常用到的框架看着学起来难,但核心是对字节码的掌握,ASM常用的核心类其实并没有几个,在ASM当中我们需要牢牢掌握的类其实就ClassReader
、ClassWriter
、ClassVisitor
、MehotdVisitor
、FieldVisitor
、AnnotationVisitor
,依次对应输入、输出、类访问器、方法访问器、字段访问器、注解访问器,这几个访问器富含到日常书写的JAVA代码,而其他的类都是在这几个核心类拓展为处理字节码提供便利的,相信看到这里应该会放下对ASM学习的压力了吧
上一篇文章中知道每个class文件可以抽象为一个具体的ClassFile
构,而ASM根据这个结构抽象出一个用于处理ClassFile
的类ClassVisitor
,类中每个方法对应着同名类文件结构部分,而类中方法调用也是按照一定的顺序进行调用,规则如:从左至右,不同符号代表不同访问次数,无符号为必访问一次。以ClassVisitor
为例,首先访问visit
方法,随后visitSource
可能被访问,再然后visitOuterClass
可能被访问,其次visitAnnotation
或visitAttribute
根据情况至少一次访问,再其次为visitInnerClass
、visitField
、visitMethod
根据情况至少一次访问,最后visitEnd
被调用,代表着一个Class访问结束。
符号 | 意义 |
---|---|
? | <= 0 |
(A|B) | 括号内部API(A、B)选择其一 |
* | >= 1 |
visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
ClassVisitor
作为ASM框架中最为核心的类ClassVisitor
,通过它我们可以访问到类中所有信息,而作为类方法返回值的AnnotationVisitor
、FieldVisitor
、MethodVisitor
负责针对注解、字段、方法更为细致的核心类,这三个Visitor的处理和ClassVisitor
的设计逻辑其实是一样的,所以当理解一个核心类的处理流程后可以融会贯通到其他的三个核心类。之前提到ASM的设计模式使用了访问者模式,而一个一个的Visitor
相当于一个一个的管道将ClassFile
的内容流向对应的一个一个的Visitor
中,从而将不同功能的Visitor
拆分并链接再一起使用,而ASM通过构造方法传参的形式将上一个Visitor
关联。
public abstract class ClassVisitor {
public ClassVisitor(final int api, final ClassVisitor classVisitor)
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 descriptor);
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible);
public void visitAttribute(Attribute attribute);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions);
public void visitEnd();
}
- void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
该方法主要提供对java版本,类权限访问符、全限定名、泛型、父类、接口相关信息 - version 由于目前minor_version为0,这个值其实就是major_version
- access
类访问标记符
- name
类全限定名
- signature
如果类存在泛型则有值,否则为null,格式如<泛型名:泛型类型全限定名> + 父类全限定名 + 接口全限定名
- superName
父类全限定名
- interfaces
实现接口全限定名数组
-
void visitSource(String source, String debug)
- source
源码文件类名
- debug
编译于源码附加调试信息对应关系
-
void visitOuterClass(String owner, String name, String descriptor)
- owner
局部类位于类的全限定名
- name
局部类位于类的方法名
- descriptor
局部类位于类的方法签名
public class OuterClass {
void someMethod(String s) {
class InnerClass {}
}
}
对于读取InnerClass
对应的值依次为:OuterClass、someMethod、(Ljava/lang/String;)V
- AnnotationVisitor visitAnnotation(String descriptor, boolean visible)
类注解信息,源码级别注解不会调用该方法,该方法需要返回一个AnnotationVisitor
,若我们需要获取注解的详细信息那么我们需要实现一个AnnotationVisitor
的子类进行单独处理 - descriptor 类注解全限定名
- visible
是否可用反射获取,即注解的Retention是RetentionPolicy.CLASS为false,为RetentionPolicy.RUNTIME为true
- void visitAttribute(Attribute attribute)
访问类的非标准属性,这个方法一般并不会接触到
- void visitInnerClass(String name, String outerName, String innerName, int access)
访问内部类信息:全限定名、外部类全限定名、类名、访问标记符
- FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value)
类或接口成员字段信息,由ClassFile中规则知道仅包括当前类或接口声明的成员字段,该方法需要返回FieldVisitor
- access 字段访问标记符
- name
字段名字
- descriptor
字段类型描述
- signature
字段泛型信息
- value
字段初始值,但仅在静态字段有效
- MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
类方法信息,由ClassFile中规则知道仅包括当前类或接口声明的方法,该方法需要返回MethodVisitor
- access
方法访问标记符
- name
方法名字
- descriptor
方法签名描述
- signature
方法相关泛型信息
- exceptions
方法异常相关信息
- void visitEnd()
类访问完成接口
ClassReader
ClassReader
的主要职责是读取class文件,将class文件内容按照ClassFile
结构转换后传递给ClassVisitor
,除了在生成类这个场景无需使用外其他的场景都需要通过ClassReader
作为数据的输入源
public class ClassReader {
public ClassReader(final byte[] classFile);
public ClassReader(final byte[] classFileBuffer,final int classFileOffset,final int classFileLength);
public ClassReader(final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion);
public ClassReader(final InputStream inputStream);
public ClassReader(final String className);
public int getAccess();
public String getClassName();
public String getSuperName();
public String[] getInterfaces();
public void accept(final ClassVisitor classVisitor, final int parsingOptions);
}
- ClassReader(final byte[] classFile)
- ClassReader(final byte[] classFileBuffer,final int classFileOffset,final int classFileLength)
- ClassReader(final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion)
- ClassReader(final InputStream inputStream)
- ClassReader(final String className)
构造函数通过不同途径读取class文件
- int getAccess()
获取类问标记符
- String getClassName()
获取类名
- String getSuperName()
获取父类名
- String[] getInterfaces()
获取实现接口
- void accept(final ClassVisitor classVisitor, final int parsingOptions)
将数据传递给ClassVisitor
- parsingOptions 解析类的选项,值为ClassReader.SKIP_CODE
、ClassReader.SKIP_DEBUG
、ClassReader.SKIP_FRAMES
、ClassReader.EXPAND_FRAMES
- SKIP_CODE 设置后跳过Code attributes
- SKIP_DEBUG
设置后跳过`SourceFile attributes`、`SourceDebugExtension attributes`、`LocalVariableTable attributes`、`LocalVariableTypeTable attributes`、`LineNumberTable attributes`的解析和处理,并且`ClassVisitor#visitSource`,`MethodVisitor#visitLocalVariable`,`MethodVisitor#visitLineNumber`将不再被调用
- SKIP_FRAMES
设置后跳过`StackMap attributes`,`StackMapTable attributes`解析和处理,并且方法`MethodVisitor#visitFrame`不在被调用。对于`ClassWriter`只有设置了`COMPUTE_FRAMES`才生效。
- EXPAND_FRAMES
设置后堆栈映射总是以拓展形式进行访问,`ClassReader`和`ClassWriter`需要额外的压缩、解压步骤导致性能降低
掌握这两个核心类后我们可以书写ASM程序的简单的例子了,以打印HelloWorld
类为例学习如何搭配使用打印class相关的字段及方法
package com.river.asm;
public class HelloWorld {
private String word = "hello world";
public void say() {
System.out.println(word);
}
public static void main(String[] args) {
new HelloWorld().say();
}
}
public class Asm {
public static void main(String[] args) throws IOException {
ClassReader classReader = new ClassReader("com/river/asm/HelloWorld");
classReader.accept(new ClassVisitor(ASM6, null) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
System.out.println(String.format("class %s {", name));
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println(String.format("\t%s %s", name, descriptor));
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println(String.format("\t%s %s", name, descriptor));
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public void visitEnd() {
super.visitEnd();
System.out.println("}");
}
}, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
}
}
代码非常简单,通过ClassReader
读取HelloWorld
并调用其accept方法将读取数据传递给ClassVisitor
,这里选择使用类全限定名作为输入源,代码中匿名ClassVisitor
负责输出类名、成员变量、方法信息。输出信息如下:
class com/river/asm/HelloWorld {
word Ljava/lang/String;
<init> ()V
say ()V
main ([Ljava/lang/String;)V
}
这两个类搭配还真的玩不出什么花样来,所以继续踏上征程吧
ClassWriter
ClassWriter
是ClassVisitor
的一个子类,主要职责是负责将ClassVisitor
处理好后经过转换生成class文件的byte数组,有了byte数组我们可以选择写入文件或者直接通过类加载器加载后进行使用
public class ClassWriter extends ClassVisitor {
public ClassWriter(final int flags)
public ClassWriter(final ClassReader classReader, final int flags)
public byte[] toByteArray() throws ClassTooLargeException, MethodTooLargeException
}
-
ClassWriter(final int flags)
- flags
设置后影响对class的默认行为,可选值为0,
ClassWriter.COMPUTE_MAXS
,ClassWriter.COMPUTE_FRAMES
- 0 需要自行计算最大方法变量池长度和最大栈深度- COMPUTE_MAXS 设置后ASM自动计算方法的最大方法变量池长度和最大栈深度,需要主义的是该flag需要有效的栈映射帧,否则请使用`COMPUTE_FRAMES`,使用后`MethodVisitor#visitMaxs`方法无效,无论程序中如何传值,ASM都走自己算法进行计算 - COMPUTE_FRAMES 设置后ASM自动计算方法的栈映射帧
栈映射帧(stack map frames)对应ClassFile
中的StackMapTable
,这个的出现是为了优化对类的加载验证。JAVA要求所有加载的类都必须进行安全验证以维护JVM的安全性,这个是在字节码级别完成的工作。之前的问题是字节码本身并不含有任何类型安全信息,类型通过数据流隐式确定,而帧栈的局部变量表存在复用变量槽的情况,使得以线性处理在遇到跳转指令后也许变量槽的类型已经被修改了,若没有任何辅助信息记录这些,需要通过多次处理字节码,无疑带来的就是对性能的影响,所以为了解决跳转带来的挑战StackMapTable
挺身而出,Oracle要求JAVA7开始的所有类必须携带相关的数据,使得验证器可以一次验证字节码。处于存储空间大小考虑,若简单的在任意点都存储值类型无疑是非常大的开销,所以为了使存储空间更合理Oracle决定仅在使用跳转指令的目标位置记录相关信息。在使用ASM时请避免自己书写相关frames代码,这个东西过于复杂,不建议自己编写,还是交给专业的ASM来处理这些问题,所以个人推荐ClassWriter.COMPUTE_FRAMES
一定要使用
言归正传,下面是通过ASM的ClassWriter
生成一个HelloWorld
类,并通过自定义ClassLoader
通过加载字节将类加载后通过反射实例化后调用say
方法
import org.apache.commons.io.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Asm {
public static void main(String[] args) throws IOException {
MethodVisitor mv;
ClassWriter cw = new ClassWriter(null, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
//生成类
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/river/asm/HelloWorld", null, "java/lang/Object", null);
//word字段
FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE, "word", "Ljava/lang/String;", null, null);
fv.visitEnd();
//构造方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitLdcInsn("hello world");
mv.visitFieldInsn(Opcodes.PUTFIELD, "com/river/asm/HelloWorld", "word", "Ljava/lang/String;");
mv.visitMaxs(0, 0);
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd();
//say方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "say", "()V", null, null);
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "com/river/asm/HelloWorld", "word", "Ljava/lang/String;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitMaxs(0, 0);
mv.visitInsn(Opcodes.RETURN);
mv.visitEnd();
//类结束
cw.visitEnd();
FileUtils.writeByteArrayToFile(new File("target\\classes\\com\\river\\asm\\HelloWorld.class"), cw.toByteArray());
ASMGenClassLoader asmGenClassLoader = new ASMGenClassLoader(cw.toByteArray());
try {
Class<?> clazz = asmGenClassLoader.loadClass("com.river.asm.HelloWorld");
Object helloWorld = clazz.newInstance();
Method say = clazz.getDeclaredMethod("say");
say.invoke(helloWorld);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
static class ASMGenClassLoader extends ClassLoader {
private byte[] bytes;
public ASMGenClassLoader(byte[] bytes) {
this.bytes = bytes;
}
@Override
protected Class<?> findClass(String name) {
return defineClass(name, bytes, 0, bytes.length);
}
}
}
FieldVisitor
public abstract class FieldVisitor {
public FieldVisitor(final int api);
public FieldVisitor(final int api, final FieldVisitor fieldVisitor);
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible);
public void visitEnd();
}
FieldVisitor
的内容非常的少,因为对代码的处理往往自动声明后都是在方法中使用,当我们重写这个的时候往往是需要对字段的注解进行相关的处理,由于过于简单和重点一般位于AnnotationVisitor
就不用 FieldVisitor举例了,后面介绍AnnotationVisitor
时搭配FieldVisitor
再行举例,这里针对字段日常对Class的处理一般是新增字段:
import org.apache.commons.io.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.IOException;
public class Asm {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("com/river/asm/HelloWorld");
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cr.accept(new ClassVisitor(Opcodes.ASM6, cw) {
@Override
public void visitEnd() {
visitField(Opcodes.ACC_PRIVATE, "word2", "Ljava/lang/String;", null, null);
super.visitEnd();
}
}, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
FileUtils.writeByteArrayToFile(new File("target\\classes\\com\\river\\asm\\HelloWorldProField.class"), cw.toByteArray());
}
}
新增字段时需要注意生成的字段名不能重复,所以在ClassVisitor
中挑选合适的方法至关重要,最好不要在多次调用的方法中新增字段,建议在visitEnd
方法,为了保证字段名唯一性可以将字段名命名按照一定复杂规则来,或者通过方法visitField
统计类中字段名后进行避免重复
AnnotationVisitor
AnnotationVisitor
的职责也非常简单用于访问注解相关内容,需要注意的是注解策略如果为源码级别的将访问不到的
public abstract class AnnotationVisitor {
public AnnotationVisitor(int api);
public AnnotationVisitor(int api, AnnotationVisitor annotationVisitor);
public void visit(String name, Object value);
public void visitEnd();
}
- void visit(String name, Object value);
该方法用于访问注解的键值对 - name 注解键值名称
- value
注解value值
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resource {
String value();
}
public class HelloWorld {
@Resource("hello river")
private String word = "hello world";
public void say() {
System.out.println(word);
}
}
public class Asm {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("com/river/asm/HelloWorld");
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cr.accept(new ClassVisitor(Opcodes.ASM6, cw) {
@Override
public FieldVisitor visitField(int access, final String name, String descriptor, String signature, Object value) {
FieldVisitor fv = super.visitField(access, name, descriptor, signature, value);
if (fv != null && name.equals("word")) {
fv = new FieldVisitor(Opcodes.ASM6, fv) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println(String.format("filed(%s) visit anno: %s",name, descriptor));
AnnotationVisitor av = super.visitAnnotation(descriptor, visible);
if (av != null) {
av = new AnnotationVisitor(Opcodes.ASM6, av) {
@Override
public void visit(String name, Object value) {
super.visit(name, value);
System.out.println(String.format("name: %s, value: %s", name, value));
}
};
}
return av;
}
};
}
return fv;
}
@Override
public void visitEnd() {
visitField(Opcodes.ACC_PRIVATE, "word2", "Ljava/lang/String;", null, null);
super.visitEnd();
}
}, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
}
}
最终输出
filed(word) visit anno: Lcom/river/asm/Resource;
name: value, value: hello river
MethodVisitor
MethodVisitor
是ASM中最为重要的类,因为这个类是对帧栈中操作数栈操作的核心类,无论实现什么功能都离不开对操作数栈的操作,而作为最为核心的类当然也是ASM中最为复杂的类,虽然咋一看涉及的方法非常多,当然若熟练掌握字节码后学习MethodVisitor
也是非常轻松的
public abstract class MethodVisitor {
public MethodVisitor(int api);
public MethodVisitor(int api, MethodVisitor methodVisitor);
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible);
public void visitCode();
public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack);
public void visitInsn(int opcode);
public void visitIntInsn(int opcode, int operand);
public void visitVarInsn(int opcode, int var);
public void visitTypeInsn(int opcode, String type);
public void visitFieldInsn(int opcode, String owner, String name, String descriptor);
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface);
public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments);
public void visitJumpInsn(int opcode, Label label);
public void visitLabel(Label label);
public void visitLdcInsn(Object value);
public void visitIincInsn(int var, int increment);
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels);
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);
public void visitMultiANewArrayInsn(String descriptor, int numDimensions);
public void visitTryCatchBlock(Label start, Label end, Label handler, String type);
public void visitMaxs(int maxStack, int maxLocals);
public void visitEnd();
}
- AnnotationVisitor visitAnnotation(String descriptor, boolean visible)
同ClassVisitor
和FieldVisitor
一样,用于访问注解信息
- descriptor
注解全限定名
- visible
是否可以通过反射获取注解
- void visitCode()
同ClassVisitor
和FieldVisitor
一样,调用该方法后开启对code的操作
- void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack)
针对跳转时记录的栈映射帧信息,不建议自己编写代码时调用该方法,建议交由ASM进行计算填充
- void visitInsn(int opcode)
执行零操作指令,意味着操作码后无需跟任何字节的操作数,例如调用mv.visitInsn(ICONST_1)
意味着将1
压入栈中 NOP
,ACONST_NULL
, ICONST_M1
, ICONST_0
, ICONST_1
, ICONST_2
, ICONST_3
, ICONST_4
, ICONST_5
,LCONST_0
, LCONST_1
, FCONST_0
, FCONST_1
, FCONST_2
, DCONST_0
, DCONST_1
, IALOAD
, LALOAD
,FALOAD
, DALOAD
, AALOAD
, BALOAD
, CALOAD
, SALOAD
, IASTORE
, LASTORE
, FASTORE
, DASTORE
,AASTORE
, BASTORE
, CASTORE
, SASTORE
, POP
, POP2
, DUP
, DUP_X1
, DUP_X2
, DUP2
, DUP2_X1
, DUP2_X2
,SWAP
, IADD
, LADD
, FADD
, DADD
, ISUB
, LSUB
, FSUB
, DSUB
, IMUL
, LMUL
, FMUL
, DMUL
, IDIV
, LDIV
,FDIV
, DDIV
, IREM
, LREM
, FREM
, DREM
, INEG
, LNEG
, FNEG
, DNEG
, ISHL
, LSHL
, ISHR
, LSHR
, IUSHR
,LUSHR
, IAND
, LAND
, IOR
, LOR
, IXOR
, LXOR
, I2L
, I2F
, I2D
, L2I
, L2F
, L2D
, F2I
, F2L
, F2D
, D2I
,D2L
, D2F
, I2B
, I2C
, I2S
, LCMP
, FCMPL
, FCMPG
, DCMPL
, DCMPG
, IRETURN
, LRETURN
, FRETURN
,DRETURN
, ARETURN
, RETURN
, ARRAYLENGTH
, ATHROW
, MONITORENTER
, MONITOREXIT
- void visitIntInsn(int opcode, int operand)
执行单个int类型操作数的指令 当操作码为 BIPUSH
时,操作数值应介于 Byte.MIN_VALUE
和 Byte.MAX_VALUE
之间。 当操作码为 SIPUSH
时,操作数值应介于 Short.MIN_VALUE
和 Short.MAX_VALUE
之间。 当操作码为 NEWARRAY
时,操作数值应为T_BOOLEAN
,T_CHAR
,T_FLOAT
,T_DOUBLE
,T_BYTE
,T_SHORT
,T_INT
,T_LONG
- void visitVarInsn(int opcode, int var)
访问局部变量指令,用于加载或存储 加载指令:ILOAD
, LLOAD
, FLOAD
, DLOAD
, ALOAD
存储指令:ISTORE
, LSTORE
, FSTORE
, DSTORE
, ASTORE
, RET
- void visitTypeInsn(int opcode, String type)
访问类型相关指令,比如将某个类型强转为另外一个类型 NEW
, ANEWARRAY
, CHECKCAST
, INSTANCEOF
- void visitFieldInsn(int opcode, String owner, String name, String descriptor)
用于访问类字段的指令,更新或者获取 GETSTATIC
, PUTSTATIC
, GETFIELD
, PUTFIELD
- owner 所属类的全限定名
- name
成员字段名字
- descriptor
成员字段类型描述
//访问静态字段
mv.visitFieldInsn(GETSTATIC, "com/river/asm/HelloWorld", "word", "Ljava/lang/String;") //将HelloWorld的静态字段word压入栈顶
//赋值静态字段
mv.visitLdcInsn("hello world") //通过LDC指令将常量hello world压入栈顶
mv.visitFieldInsn(PUTSTATIC, "com/river/asm/HelloWorld", "word", "Ljava/lang/String;") //将hello world赋值给HelloWorld的静态字段word
//访问成员字段
mv.visitVarInsn(ALOAD, 0) //加载this压入栈顶
mv.visitFieldInsn(GETFIELD, "com/river/asm/HelloWorld", "word", "Ljava/lang/String;") //将HelloWorld的成员字段word压入栈顶
//复制成员字段
mv.visitVarInsn(ALOAD, 0)
mv.visitLdcInsn("hello world") //通过LDC指令将常量hello world压入栈顶
mv.visitFieldInsn(PUTFIELD, "com/river/asm/HelloWorld", "word", "Ljava/lang/String;") //将hello world赋值给HelloWorld的字段word
- void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface)
用于访问类方法指令 INVOKEVIRTUAL
, INVOKESPECIAL
, INVOKESTATIC
, INVOKEINTERFACE
mv.visitVarInsn(ALOAD, 0) //加载this压入栈顶
mv.visitMethodInsn(INVOKEVIRTUAL, "com/river/asm/HelloWorld", "say", "()V", false) //访问方法say
mv.visitMethodInsn(INVOKESPECIAL, "com/river/asm/HelloWorld", "<init>", "()V", false) //访问HelloWorld构造方法
mv.visitMethodInsn(INVOKESTATIC, "com/river/asm/HelloWorld", "say", "()V", false) //访问静态方法say
- void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments)
用于访问动态句柄指令,非常强大,lambda函数式便是借助这个指令实现的
- void visitJumpInsn(int opcode, Label label)
用于控制代码跳转的指令 FEQ
,IFNE
, IFLT
, IFGE
, IFGT
, IFLE
, IF_ICMPEQ
, IF_ICMPNE
, IF_ICMPLT
, IF_ICMPGE
, IF_ICMPGT
,IF_ICMPLE
, IF_ACMPEQ
, IF_ACMPNE
, GOTO
, JSR
, IFNULL
, IFNONNULL
Label label = new Label()
mv.visitJumpInsn(IFNE, label) //栈顶int值若等于0时直接执行到mv.visitLabel
mv.visit...
...
mv.visitLabel(label)
- void visitLabel(Label label)
用于书写代码跳转标记的方法
- void visitLdcInsn(Object value)
用于加载的LDC
指令
- void visitIincInsn(int var, int increment)
用于访问IINC
指令
- void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels)
用于访问TABLESWITCH
指令
- void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)
用于访问LOOKUPSWITCH
指令
- void visitMultiANewArrayInsn(String descriptor, int numDimensions)
用于访问MULTIANEWARRAY
指令
- void visitTryCatchBlock(Label start, Label end, Label handler, String type)
用于异常捕获的方法
- void visitMaxs(int maxStack, int maxLocals)
设置方法的操作数栈的最大深度和最大局部变量池长度
- void visitEnd()
调用该方法后方法访问结束
这里以在say方法新增一条输出为例:
import org.apache.commons.io.FileUtils;
import org.objectweb.asm.*;
import java.io.File;
import java.io.IOException;
public class Asm {
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("com/river/asm/HelloWorld");
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cr.accept(new ClassVisitor(Opcodes.ASM6, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && name.equals("say")) {
mv = new MethodVisitor(Opcodes.ASM6, mv) {
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
visitLdcInsn("hello river");
visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
}, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
FileUtils.writeByteArrayToFile(new File("target\\classes\\com\\river\\asm\\HelloWorld.class"), cw.toByteArray());
}
}
需要注意的时在方法visitInsn
中仅仅判断了RETURN
,在真实方法执行退出插入代码这样写时错误的,但是我们这边仅仅使用HelloWorld
就没有问题了。至此我们对ASM中的关键核心类已经率为有所了解,但是相信想书写什么功能时也无从下手,所以下一篇文章着重寻找一些案例来编写一些真实的场景进一步学习ASM。