字节码&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通过加载和执行同一种于平台无关的字节码,而字节码由不同语言通过编译后得到的产物

0.svg

JVM指令由一个字节的操作码(opcode)和紧随其后的可选操作数(operand)构成,比如: opcode operand1, operand2 而JVM执行指令的过程其实就是不断将opcodeoperand出栈和入栈的过程,比如a = 1就是通过将变量a压入执行栈后再将1压入执行栈后再通过存储指令将a进行存储

1.svg

字节码名字的由来是因为操作码的大小为一个字节,所以操作码集最多由256个,而目前已经使用了超过200个了,虽然操作码的数量已经相当的庞大,但是其中有着很多类似的操作码,这是因为操作码大多是于类型有关的,比如对变量存储的操作码就有ISTORELSTOREFSTOREDSTOREASTOREIASTORELASTOREFASTOREDASTOREAASTOREBASTORECASTORESASTORE这13个操作码,不难发现其中一般为类型描述符 + STORE

JAVA虚拟机栈和栈帧

虚拟机实现方式常见的有两种:基于栈和基于寄存器。基于栈的虚拟机有大名鼎鼎的Hotspot JVM,基于寄存器的虚拟机有LuaLua 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指令将R0R1中的值求和存储到R2,第二行返回R2的值,第三行则为lua特殊的处理为了防止分支遗漏return指令

基于栈和基于寄存器架构优缺点:

  • 基于栈移植性好、指令更短、实现简单,但不能随机访问堆栈中元素,完成相同功能比基于寄存器所需要的指令数一般都要多,需要频繁出入栈
  • 基于寄存器速度快,可以充分利用寄存器,操作数需要显示指定,指令较长

帧栈

Hotspot JVM是一款基于栈的虚拟机,每个线程都有一个虚拟机栈用来存储栈帧,每次方法的调用都将伴随着栈帧的创建、销毁。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧的存储控件分配在JAVA虚拟机栈中,每个栈帧都拥有自己的局部变量表(Local Variable)、操作数栈、常量池的引用。

2.svg

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;

3.svg

局部变量表

每个栈帧内部都包含一组称为局部变量表的变量列表,局部变量表长度在编译期间确定,对应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占用两个变量索引位置,所以尽量就使用POP2DUP2...

运算操作符

操作符名称含义
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 时跳转

这里需要指出的是最好记住EQNELTGEGTLE这几个单词的含义。

跳转控制操作符

操作符名称含义
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当中我们需要牢牢掌握的类其实就ClassReaderClassWriterClassVisitorMehotdVisitorFieldVisitorAnnotationVisitor,依次对应输入、输出、类访问器、方法访问器、字段访问器、注解访问器,这几个访问器富含到日常书写的JAVA代码,而其他的类都是在这几个核心类拓展为处理字节码提供便利的,相信看到这里应该会放下对ASM学习的压力了吧

上一篇文章中知道每个class文件可以抽象为一个具体的ClassFile构,而ASM根据这个结构抽象出一个用于处理ClassFile的类ClassVisitor,类中每个方法对应着同名类文件结构部分,而类中方法调用也是按照一定的顺序进行调用,规则如:从左至右,不同符号代表不同访问次数,无符号为必访问一次。以ClassVisitor为例,首先访问visit方法,随后visitSource可能被访问,再然后visitOuterClass可能被访问,其次visitAnnotationvisitAttribute根据情况至少一次访问,再其次为visitInnerClassvisitFieldvisitMethod根据情况至少一次访问,最后visitEnd被调用,代表着一个Class访问结束。

符号意义
? <= 0
(A|B) 括号内部API(A、B)选择其一
* >= 1
visit 
visitSource? 
visitOuterClass? 
( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd

ClassVisitor

作为ASM框架中最为核心的类ClassVisitor,通过它我们可以访问到类中所有信息,而作为类方法返回值的AnnotationVisitorFieldVisitorMethodVisitor负责针对注解、字段、方法更为细致的核心类,这三个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_CODEClassReader.SKIP_DEBUGClassReader.SKIP_FRAMESClassReader.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

ClassWriterClassVisitor的一个子类,主要职责是负责将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)

ClassVisitorFieldVisitor一样,用于访问注解信息

- descriptor
注解全限定名

- visible
是否可以通过反射获取注解
  • void visitCode()

ClassVisitorFieldVisitor一样,调用该方法后开启对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_VALUEByte.MAX_VALUE 之间。 当操作码为 SIPUSH 时,操作数值应介于 Short.MIN_VALUEShort.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。

 
posted @ 2022-03-17 23:48  人生的激活码  阅读(369)  评论(0编辑  收藏  举报