ASM指南通译-10 生成方法

ASM指南翻译-10 生成方法

 

3.2接口和组件

3.2.1表现

ASM API中关于方法的生成和转换的部分是基于MethodVisitor接口的(见图3.4),该接口由ClassVisitorvisitMethod方法返回。除了一些与注解和调试相关的方法之外,这些将在后面章节中介绍,MethodVisitor接口还针对每个字节码指令分类定义了一个方法,这个分类是基于这些指令的编号以及参数的类型(这些分类与3.1.2章节中出现的分类并不对应)。这些方法必须按照下面给定的顺序来调用(还包含一些定义在MethodVisitor接口的JavaDoc文档中的限制):

visitAnnotationDefault?

( visitAnnotation | visitParameterAnnotation | visitAttribute )*

( visitCode

         ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |

         visitLocalVariable | visitLineNumber )*

         visitMaxs )?

         visitEnd

 

这就意味着,如果存在注解和属性,那么它们必须首先被调用,然后才是方法的字节码,这只针对非抽象方法。位于visitCodevisitMax方法之间的这些方法,必须按照顺序来调用,并且visitCodevisitMax只能调用一次。

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接口

在一系列的事件中,visitCodevisitMaxs可以被用来检测方法字节码的开始和结束。就像类一样,visitEnd方法必须最后被调用,同时可以用来检测类的结束。

 

ClassVisitorMethodVisitor可以一定的顺序合并起来以生成更复杂的类:

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实例被当做参数传递给ClassReaderaccept方法。
  • ClassWritervisitMethod方法返回MethodVisitor接口的一个实现,由它来直接以二进制的方式构建编译后的方法。
  • MethodAdapterMethodVisitor的一个实现,它将自己接受的方法调用委托给其他的MethodVisitor实例。

 

ClassWriter的选项

 

如我们在3.1.5章节中所见,计算一个方法的栈映射帧并不容易:你必须计算所有的帧,找到与跳转目标以及那些无条件跳转想对应的帧,最后压缩这些帧。同样地,计算一个方法的局部变量和操作数栈的大小也不容易,虽然相对而言容易点。

 

令人高兴地是,ASM可以为你计算这些东西。当你创建一个ClassWriter时,你可以指定哪些是需要自动计算的:

 

  • new  ClassWriter(0) 将不会自动进行计算。你必须自己计算帧、局部变量和操作数栈的大小。
  • new ClassWriter(ClassWriter.COMPUTE_MAXS) 局部变量和操作数栈的大小就会自动计算。但是,你仍然需要自己调用visitMaxs方法,尽管你可以使用任何参数:实际上这些参数会被忽略,然后重新计算。使用这个选项,你仍然需要计算帧的大小。
  • new ClassWriterClassWriter.COMPUTE_FRAMES 所有的大小都将自动为你计算。你也不许要调用visitFrame方法,但是你仍然需要调用visitMaxs方法(参数将被忽略然后重新计算)。

 

使用这些选项是方便很多,但是会带来一些损失:COMPUTE_MAXS选项将使得ClassWriter10%,使用COMPUTE_FRAMES选项将使得ClassWriter慢两倍。你必须将它与自己计算花费的时间相比较:与ASM提供的算法相比较,在某些特定的情况下,这里有一些更容易和快速的算法来进行计算,但是必须处理所有的情形。

 

如果你选择自己计算这些帧的大小,你可以让ClassWriter来帮你进行帧压缩。这样的话,你只能使用visitFrame(F_NEW,nLocals,locals,nStack,stack)方法去访问那些未被压缩的帧,nLocalsnStack代表局部变量和操作数栈的大小,localsstack是一些包含对应类型的数组(参看JavaDoc)。

 

注意,为了自动计算帧的大小,有时必须计算两个类共同的父类。缺省情况下,ClassWriter将会在getCommonSuperClass方法中计算这些,通过在加载这两个类进入虚拟机时,使用反射API来计算。但是,如果你将要生成的几个类相互之间引用,这将会带来问题,因为引用的类可能还不存在。在这种情况下,你可以重写getCommonSuperClass方法来解决这个问题。

 

 

3.2.2生成方法

 

3.1.3节中定义的getF方法字节码可以通过下面的方法调用来生成,其中mvMethodVisitor

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();

 

visitCodevisitEnd调用之间的方法调用和3.1.5节最后展示的字节码是精确映射的。每个调用对应一个指令,标签或者帧(唯一的例外就是labelend label的声明和构造)。

 

注意 一个label对象指定的指令就是那些紧跟在这个labelvisitLabel方法调用之后。例如,end指定了RETURN指令,不是其后的frame,因为它不是一个指令。可以使用几个标签来指定相同的指令,但是一个标签必须指定一个指令。换句话说,可以针对不同的label连续调用visitLabel,但是在一个指令中使用的标签必须只能被visitLabel访问一次。最后一个限制就是标签不能共享,每个方法都有自己的标签。