3、使用ASM API生成和转换class中的方法
前面已经简单介绍过已编译的class文件中的方法是由字节码指令序列构成,因此使用ASMAPI生成和转换class文件中的方法需要具备基本的字节码指令知识和基本的字节码指令运行原理。

3.1、执行模型
我们知道java代码是运行在java虚拟机的线程中,每一个线程拥有各自的执行栈,执行栈由多个帧组成。每个帧可以代表一个方法的运行过程,当方法被触发时,一个新的帧将会被push到当前线程的执行栈中。当方法正常返回或者发生异常,当前线程会将该方法帧pop出执行栈。

每个帧由本地变量和操作栈两部分组成。本地变量部分由一个数组组成,可通过数据下标访问变量。操作栈部分是由一组字节码指令序列组成,指令序列将按照后进先出的顺序访问。每一个在线程执行栈中的帧都包括它自己的操作栈。

帧中的本地变量数组和操作栈的大小由类中方法的代码决定,在类编译过程中计算帧的内容并将它以字节码指令的方式储存在已编译的class文件中。在同一方法的执行过程中,该方法对应的每一帧的大小相同,但是不同的方法帧大小可以不同,它们的大小由本地变量数组和操作栈部分的大小决定。

下图展示了一个执行栈中帧的例子,帧1包含3个本地变量,它的操作栈的最大容量为4;帧2包含2个本地变量,它的操作栈中有2个值;同样的,帧3中包含4个本地变量和2个操作栈值;

图3.1一个执行栈中的3个帧

当非静态(static)方法被创建时,执行栈初始化一个新的帧,该帧初始化它的操作栈并且将this和方法的参数压入到它的本地变量数组中。比如执行a.equals(b)时,线程执行栈中初始化一个帧,初始化帧的操作栈并且将a,b参数压入到帧的本地变量数组中。任何java类型的变量都能被加入到帧的本地变量数组中,但是有别于其他类型的变量只在本地变量数组中占一个单位的位置,long和double类型有点特别,在加入后它们在数组中各占2个单位的位置。因此第i个方法的参数不一定存储在帧本地变量数组的第i个位置。比如Math.max(1L,2L),在本地变量数组中占4个单位的位置,1L和2L各占2个单位的位置,本地变量数组的长度为4。

3.2、字节码指令
一个字节码指令由操作码和固定数量个参数组成。操作码是一个非负整数,因此字节码指令的名字由一个数字来标识。比如操作码0使用符号NOP来助记,其中NOP指令不做任何事情。操作码后一般跟参数,用来定义指令具体的行为,比如助记符号GOTO(操作码167)后跟的参数用来指定下一条要执行的指令位置。

字节码指令主要分成两类,一类用来对变量进行从帧本地变量数组到操作栈之间的正反向转化;另一类在操作栈中对变量进行操作,比如从栈中pop出一个值,做某种计算后再重新把该值push回操作栈中。

1、用来进行做转换的指令:ILOAD,LLOAD, FLOAD, DLOAD,和ALOAD指令用来读取帧本地变量数组中的值然后push到操作栈;ILOAD指令用来读取boolean,byte, char, short或者int类型的变量;LLOAD,FLOAD, DLOAD指令用来读取long,float, double类型的变量;ALOAD指令用来读取非原始类型变量,比如对象,数组的引用等;同时ISTORE,LSTORE, FSTORE, DSTORE和ASTORE指令用来将相应类型的变量从操作栈中pop,然后存储到帧本地变量数组中。

2、用来在操作栈中运算的指令;
l  栈操作:POP, PUSH, DUP, SWAP
l  常量:ACONST_NULL(push null到操作栈),ICONST_0(pushint类型的0到操作栈),FCONST_0,DCONST_0,BIPUSHb(push byte类型的b到操作栈),SIPUSHs(push short类型的s到操作栈),LDCcst(push int,float,long,double,String,class常量到操作栈)
l  算数逻辑,从操作栈中pop出值做完相应的算数逻辑运算后再push回操作栈,涉及到的指令有:xADD,xSUB, xMUL, xDIV和xREM(分别对应+,-,*,/,%计算逻辑)
l  类型转换,从操作栈中pop出值做完相应的类型转换后再push回操作栈,涉及到的指令有:I2F,F2D, L2D(分别对应int转float,float转double,long转double操作),CHECKCASTt(将原引用类型转换成t类型)
l  对象,创建,锁定,测试一个对象:NEW type(push 一个新的type类型的对象到操作栈中)
l  fields:GETFIELD owner name desc(pop对象的引用,pushname的值到操作栈中),PUTFIELDowner name desc(pop对象的值和引用,将值存储到name的field),GETSTATIC和PUTSTATIC指令用来操作静态对象,作用一样;
l  方法,该类指令用来触发一个方法或者构造函数,先pop出对应数量的参数,方法运算后,push方法的返回值到操作栈中:INVOKEVIRTUALowner name desc(指令用来触发ownerclass中的name方法),INVOKESTATICowner name desc(指令用来触发静态方法),同理INVOKESPECIAL指令用来触发类中的私有方法和构造函数,INVOKEINTERFACE指令用来触发接口方法
l  数组,用来读写数组中的值;xALOAD(pop index和array,push array中index个位置值到操作栈中),xASTORE(popindex, value和array,存储value到array中的index个位置上)
l  条件判断:IFEQ label(pop出一个int值然后判断是否为0,若为0则跳转到label位置),同理IFGE,IFNE,TABLESWITCH,LOOKUPSWITCH
l  返回,终止一个方法的执行并返回结果给caller:xRETURN和RETURN分别返回对应类型的值和void

3.3、例子

1)、getF方法的字节码指令:

具体执行过程:首先从帧本地变量数组中读取下标为0的值(初始化帧时设置为this)push到操作栈中,然后pop出this,pushthis的pkg/Bean类f对象到操作栈中(this.f),最后从操作栈中pop出值并返回给caller,该帧每阶段的变化如下图所示。

图3.2(a)初始化状态,(b)执行ALOAD0后,(c)执行GETFIELD指令后

2)、setF方法的字节码指令:

具体执行过程:第一条指令从帧本地变量数组中读取下标为0的值(初始化帧时设置为this)push到操作栈中;第二条指令从帧本地变量数组中读取下标为1的值push到操作栈中用作赋值的int值;第三条指令pop出this和int值, 存储pkg/Bean类f对象的引用(也就是this.f)到本地变量数组中,第四条指令返回void到caller,该帧每阶段的变化如下图所示。

图3.3(a)初始化状态,(b)执行ALOAD0后,(c)执行LOAD1后,(c)执行PUTFIELD指令后

3)、Bean的默认构造函数Bean(){ super(); }字节码指令:

具体执行过程:第一条指令从帧本地变量数组中读取下标为0的值(初始化帧时设置为this)push到操作栈中;第二条指令pop出this调用java/lang/Object类的<init>方法;第三条指令返回void到caller。
3.4、更复杂的例子

字节码指令如下:

3.4.1、捕获异常的例子

字节码指令如下:

3.5、ASM接口和组件
使用ASMAPI产生和转换class文件的方法是基于MethodVisitor抽象类进行的,其中MethodVisitor通过ClassVisitor的visitMethod方法返回。MethodVisitor类中定义了许多访问class文件的方法内容和属性的方法;其中,使用继承自MethodVisitor的类对class文件的方法内容和属性的访问顺序如下;class文件的方法的注释和属性被先访问,随后才是访问目标method的bytecode;另外,visitCode和visitMaxs方法可以被用来判断class文件方法字节码指令序列的开始和结束;

MethodVisitor类的主要方法:

ClassVisitor和MethodVisitor结合起来使用,产生一个class文件的大概过程;

ClassReader、ClassWriter和MethodVisitor结合起来使用,转换一个class文件方法;ClassReader载入class文件,在调visitMethod方法时会返回一个继承自MethodVisitor类的对象,在这里用自己实现的MethodVisitor子类对象作为返回值返回即可,其中,在自己的类中实现各种对目标class方法的指令转换工作;最后ClassWriter输出新的class字节码文件。
3.6、产生方法

产生getF()方法:(方法的字节码指令请见3.3.1)

产生checkAndSetF方法:(方法的字节码指令请见3.4)

3.7、转换方法
去掉class方法(非构造函数)中NOP指令的例子
实现继承自MethodVisitor的类适配器(methodadapter)

实现继承自ClassVisitor的类适配器(classadapter)

class文件的方法转换时序图;

图3.4RemoveNopAdapter的时序图

3.8、无状态的转换(当前指令执行状态不依赖其他指令状态,不需要在methodAdapter中保存指令状态)
为class文件中方法增加计时器的例子;
实现效果相当:

AddTimerAdapter的实现;

3.9、有状态的转换(本指令依赖上个指令的内容)
例子,去掉ALOAD0 ALOAD 0 GETFIELD f PUTFIELD f这样的指令
状态机图如下;

图3.5 ALOAD0 ALOAD 0 GETFIELD f PUTFIELD f指令的状态机

代码实现: