?
3.2.4无状态转换
假定,我们想要测量一个程序中花费在每个类上的时间,那么我们需要在每个类上添加一个静态的计时字段,并且我们需要将每个方法的执行之间加到这个字段上。也就是说我们想转换类似于C这样的一个类:
public class C {
???????? public void m() throws Exception {
???????? ???????? Thread.sleep(100);
???????? }
}
转换之后:
public class C {
???????? public static long timer;
???????? public void m() throws Exception {
?????????????????? timer -= System.currentTimeMillis();
?????????????????? Thread.sleep(100);
?????????????????? timer += System.currentTimeMillis();
???????? }
}
?
为了弄清楚ASM是如何实现的,我们将编译这两个类,然后对比它们的TraceClassVisitor或者ASMifierClassVisitor的输出。通过TraceClassVisitor我们可以发现以下不同(粗体表示):
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LSUB
PUTSTATIC C.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LADD
PUTSTATIC C.timer : J
RETURN
MAXSTACK = 4
MAXLOCALS = 1
?
通过上面我们看到了,我们必须在这个方法的最开始添加四条指令,以及在这个方法返回之前添加另外四条指令。最后 我们需要更新操作数栈的大小。因此,我们可以在方法适配器中重写这个方法以增加最开始的四条指令:
public void visitCode() {
???????? mv.visitCode();
???????? mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
???????? mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
?????????????????? "currentTimeMillis", "()J");
???????? mv.visitInsn(LSUB);
???????? mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
?
在这里owner必须设置为即将转换的类的名字。现在我们需要添加另外四条位于RETURN之前的指令,同时这四条指令也必须位于xRETURN或者ATHROW之前,因为这些指令都会结束方法的执行。这些指令没有任何参数,并且都是通过visitInsn方法来访问。因此,我们可以重写这个方法来添加这四条指令:
public void visitInsn(int opcode) {
???????? if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
?????????????????? mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
?????????????????? mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
?????????????????? "currentTimeMillis", "()J");
?????????????????? mv.visitInsn(LADD);
?????????????????? mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
???????? }
???????? mv.visitInsn(opcode);
}
?
最后,我们仍然需要更新操作数栈的大小。这些指令中,我们往操作数栈中放置了两个long行数据,因此需要四个方框。在这个方法开始的时候,这个操作数栈是被初始化为空的,因此我们知道最开始的四条指令需要的栈大小为4.我们也知道,我们插入的代码对这个栈大小没有改变(因为从栈中弹出的数据与入栈的数据一样多)。结果是,如果最初的代码需要的栈大小为s,那么转换方法之后栈的大小的最大值为max(4,s)。不幸地是,我们还需要在RETURN指令之前添加四条指令,在这里我们不知道这个操作数栈的大小。我们只知道它的大小应该小于等于s。最后,我们可以认为在RETURN指令之前添加的代码需要的操作数栈的大小最大为s+4。这种最坏的情形在实际中很少发生:就一般的编译器而言,RETURN指令之前,操作数栈中一般只包含方法的返回值,那么它的大小就可能是0,1或者最大为2.但是,如果我们希望处理所有可能的情况,那么我们应该使用最坏的这种情况。所以,我们需要重写visitMaxs方法:
public void visitMaxs(int maxStack, int maxLocals) {
???????? mv.visitMaxs(maxStack + 4, maxLocals);
}
?
当然了,我们也不必为操作数栈的大小而烦恼,我们可以依赖于COMPUTE_MAXS选项,它会为我们计算最优的值而不是最坏的值。同时,就这么一个简单的转换,也没必须花费这么经理来手动更新maxStack。
?
现在,一个有趣的问题是:如何处理栈映射帧?原始的代码不包含任何帧,转换后的代码页不包含,难道这是因为我们使用的例子很特殊吗?这里是否有一些情形帧必须被更新吗?答案是否定的,因为,第一,插入的代码没有改变操作数栈,第二,插入的代码没有包含跳转指令,第三,这些跳转指令,或者更正式的,程序的控制流没有修改。这意味着原始的帧没有改变,既然新插入的代码没有增加帧,那么原始帧压缩后也没有改变。
?
现在,我们可以将这些元素和ClassAdapter以及MethodAdapter结合起来:
public class AddTimerAdapter extends ClassAdapter {
???????? private String owner;
???????? private boolean isInterface;
???????? public AddTimerAdapter(ClassVisitor cv) {
?????????????????? super(cv);
???????? }
???????? @Override public void visit(int version, int access, String name,
?????????????????? String signature, String superName, String[] interfaces) {
?????????????????? cv.visit(version, access, name, signature, superName, interfaces);
?????????????????? owner = name;
?????????????????? isInterface = (access & ACC_INTERFACE) != 0;
???????? }
???????? @Override public MethodVisitor visitMethod(int access, String name,
?????????????????? String desc, String signature, String[] exceptions) {
?????????????????? MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
?????????????????? exceptions);
?????????????????? if (!isInterface && mv != null && !name.equals("<init>")) {
??????????????????????????? mv = new AddTimerMethodAdapter(mv);
?????????????????? }
???????? return mv;
???????? }
???????? @Override public void visitEnd() {
?????????????????? if (!isInterface) {
??????????????????????????? FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer",
??????????????????????????? "J", null, null);
??????????????????????????? if (fv != null) {
???????????????????????????????????? fv.visitEnd();
??????????????????????????? }
?????????????????? }
???????? cv.visitEnd();
}
?
class AddTimerMethodAdapter extends MethodAdapter {
???????? public AddTimerMethodAdapter(MethodVisitor mv) {
?????????????????? super(mv);
???????? }
???????? @Override public void visitCode() {
?????????????????? mv.visitCode();
?????????????????? mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
?????????????????? mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
?????????????????? "currentTimeMillis", "()J");
?????????????????? mv.visitInsn(LSUB);
?????????????????? mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
???????? }
???????? @Override public void visitInsn(int opcode) {
?????????????????? if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
??????????????????????????? mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
??????????????????????????? mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
??????????????????????????? "currentTimeMillis", "()J");
??????????????????????????? mv.visitInsn(LADD);
??????????????????????????? mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
?????????????????? }
?????????????????? mv.visitInsn(opcode);
???????? }
???????? @Override public void visitMaxs(int maxStack, int maxLocals) {
?????????????????? mv.visitMaxs(maxStack + 4, maxLocals);
???????? }
???????? }
}
?
这里的类适配器是用来实例化方法适配器(除了构造方法),同时也用来添加计时字段和保存将被转换的类名到一个字段中,这样在方法适配器中可以访问到这个类名。