ASM指南通译-10 生成方法
3.2接口和组件
3.2.1表现
ASM API中关于方法的生成和转换的部分是基于MethodVisitor接口的(见图3.4),该接口由ClassVisitor的visitMethod方法返回。除了一些与注解和调试相关的方法之外,这些将在后面章节中介绍,MethodVisitor接口还针对每个字节码指令分类定义了一个方法,这个分类是基于这些指令的编号以及参数的类型(这些分类与3.1.2章节中出现的分类并不对应)。这些方法必须按照下面给定的顺序来调用(还包含一些定义在MethodVisitor接口的JavaDoc文档中的限制):
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
这就意味着,如果存在注解和属性,那么它们必须首先被调用,然后才是方法的字节码,这只针对非抽象方法。位于visitCode和visitMax方法之间的这些方法,必须按照顺序来调用,并且visitCode和visitMax只能调用一次。
public interface MethodVisitor {
AnnotationVisitor visitAnnotationDefault();
AnnotationVisitor visitAnnotation(String desc, boolean visible);
AnnotationVisitor visitParameterAnnotation(int parameter,
String desc, boolean visible);
void visitAttribute(Attribute attr);
void visitCode();
void visitFrame(int type, int nLocal, Object[] local, int nStack,
Object[] stack);
void visitInsn(int opcode);
void visitIntInsn(int opcode, int operand);
void visitVarInsn(int opcode, int var);
void visitTypeInsn(int opcode, String desc);
void visitFieldInsn(int opc, String owner, String name, String desc);
void visitMethodInsn(int opc, String owner, String name, String desc);
void visitJumpInsn(int opcode, Label label);
void visitLabel(Label label);
void visitLdcInsn(Object cst);
void visitIincInsn(int var, int increment);
void visitTableSwitchInsn(int min, int max, Label dflt,
Label labels[]);
void visitLookupSwitchInsn(Label dflt, int keys[], Label labels[]);
void visitMultiANewArrayInsn(String desc, int dims);
void visitTryCatchBlock(Label start, Label end, Label handler,
String type);
void visitLocalVariable(String name, String desc, String signature,
Label start, Label end, int index);
void visitLineNumber(int line, Label start);
void visitMaxs(int maxStack, int maxLocals);
void visitEnd();
}
图3.4 MethodVisitor接口
在一系列的事件中,visitCode和visitMaxs可以被用来检测方法字节码的开始和结束。就像类一样,visitEnd方法必须最后被调用,同时可以用来检测类的结束。
ClassVisitor和MethodVisitor可以一定的顺序合并起来以生成更复杂的类:
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();
注意,这里没有必要结束一个方法的访问之后再去访问另外一个,事实上,MethodVisitor接口是完全独立的,它们可以任何顺序来使用,只要cv.visitEnd()方法还未被调用:
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();
ASM提供了三个基于MethodVisitor接口的组件来生成和转换方法:
- ClassReader解析编译后方法的内容,然后调用MethodVisitor接口中对应的方法,其中MethodVisitor实例由ClassVisitor返回,而ClassVisitor实例被当做参数传递给ClassReader的accept方法。
- ClassWriter的visitMethod方法返回MethodVisitor接口的一个实现,由它来直接以二进制的方式构建编译后的方法。
- MethodAdapter是MethodVisitor的一个实现,它将自己接受的方法调用委托给其他的MethodVisitor实例。
ClassWriter的选项
如我们在3.1.5章节中所见,计算一个方法的栈映射帧并不容易:你必须计算所有的帧,找到与跳转目标以及那些无条件跳转想对应的帧,最后压缩这些帧。同样地,计算一个方法的局部变量和操作数栈的大小也不容易,虽然相对而言容易点。
令人高兴地是,ASM可以为你计算这些东西。当你创建一个ClassWriter时,你可以指定哪些是需要自动计算的:
- new ClassWriter(0) 将不会自动进行计算。你必须自己计算帧、局部变量和操作数栈的大小。
- new ClassWriter(ClassWriter.COMPUTE_MAXS) 局部变量和操作数栈的大小就会自动计算。但是,你仍然需要自己调用visitMaxs方法,尽管你可以使用任何参数:实际上这些参数会被忽略,然后重新计算。使用这个选项,你仍然需要计算帧的大小。
- new ClassWriter(ClassWriter.COMPUTE_FRAMES) 所有的大小都将自动为你计算。你也不许要调用visitFrame方法,但是你仍然需要调用visitMaxs方法(参数将被忽略然后重新计算)。
使用这些选项是方便很多,但是会带来一些损失:COMPUTE_MAXS选项将使得ClassWriter慢10%,使用COMPUTE_FRAMES选项将使得ClassWriter慢两倍。你必须将它与自己计算花费的时间相比较:与ASM提供的算法相比较,在某些特定的情况下,这里有一些更容易和快速的算法来进行计算,但是必须处理所有的情形。
如果你选择自己计算这些帧的大小,你可以让ClassWriter来帮你进行帧压缩。这样的话,你只能使用visitFrame(F_NEW,nLocals,locals,nStack,stack)方法去访问那些未被压缩的帧,nLocals和nStack代表局部变量和操作数栈的大小,locals和stack是一些包含对应类型的数组(参看JavaDoc)。
注意,为了自动计算帧的大小,有时必须计算两个类共同的父类。缺省情况下,ClassWriter将会在getCommonSuperClass方法中计算这些,通过在加载这两个类进入虚拟机时,使用反射API来计算。但是,如果你将要生成的几个类相互之间引用,这将会带来问题,因为引用的类可能还不存在。在这种情况下,你可以重写getCommonSuperClass方法来解决这个问题。
3.2.2生成方法
在3.1.3节中定义的getF方法字节码可以通过下面的方法调用来生成,其中mv为MethodVisitor:
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
从第一个方法开始进行字节码生成,紧跟着的三个方法调用用来生成方法中的代码(字节码和ASM API之间的映射还是很简单的)。visitMaxs方法必须在其他方法被调用之后再调用,它是用来这个方法的执行帧中局部变量和操作数栈的大小的。如在3.1.3节中见到的,一个方框表示一部分。最后的方法调用是用来结束方法的生成。
相应地,setF方法和构造方法也可以相似的方式来生成。一个比较有意思的例子是checkAndSetF方法:
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
mv.visitJumpInsn(GOTO, end);
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
在visitCode和visitEnd调用之间的方法调用和3.1.5节最后展示的字节码是精确映射的。每个调用对应一个指令,标签或者帧(唯一的例外就是label和end label的声明和构造)。
注意 一个label对象指定的指令就是那些紧跟在这个label的visitLabel方法调用之后。例如,end指定了RETURN指令,不是其后的frame,因为它不是一个指令。可以使用几个标签来指定相同的指令,但是一个标签必须指定一个指令。换句话说,可以针对不同的label连续调用visitLabel,但是在一个指令中使用的标签必须只能被visitLabel访问一次。最后一个限制就是标签不能共享,每个方法都有自己的标签。