# 3.3 工具

org.objectweb.asm.commons 包中包含了一些预定义的方法适配器,可用于定义我们自己的适配器。这一节将介绍其中的三个,并用 3.2.4 节的 AddTimerAdapter 示例说明如何使用它们。我们还说说明,如何利用上一章看到的工具来简化方法生成或转换。

# 3.3.1 基本工具

2.3 节介绍的工具也可用于方法。

  1. Type 许多字节代码指令,比如 xLOADxADDxRETURN 依赖于将它们应用于哪种类型。Type 类提供了一个 getOpcode 方法,可用于为这些指令获取与一给定类型相对应的操作码。这一方法的参数是一个 int 类型的操作码,针对哪种类型调用该方法,则返回该哪种类型的操作码。例如 t.getOpcode(IMUL),若 t 等于 Type.FLOAT_TYPE,则返回 FMUL

  2. TraceClassVisitor 这个类在上一章已经介绍过,它打印它所访问类的文本表示,包括类的方法的文本表示,其方式非常类似于这一章使用的方式。因此,可以将它用来跟踪在一个转换链中任意点处所生成或所转换方法的内容。例如:

java -classpath asm.jar:asm-util.jar \ 
org.objectweb.asm.util.TraceClassVisitor \ 
java.lang.Void
1
2
3

将输出:

// class version 49.0 (49)
// access flags 49
public final class java/lang/Void {
// access flags 25
// signature Ljava/lang/Class<Ljava/lang/Void;>;
// declaration: java.lang.Class<java.lang.Void> public final static Ljava/lang/Class; TYPE
// access flags 2 private <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V RETURN
MAXSTACK = 1
MAXLOCALS = 1
// access flags 8 static <clinit>()V
LDC "void"
INVOKESTATIC java/lang/Class.getPrimitiveClass (...)... PUTSTATIC java/lang/Void.TYPE : Ljava/lang/Class; RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

它说明如何生成一个静态块 static { ... },也就是用<clinit>方法(用于 CLass INITializer)。注意,如果希望跟踪某一个方法在链中某一点处的内容,而不是跟踪类的所有内容,可以用 TraceMethodVisitor 代替 TraceClassVisitor(在这种情况下,必须显式指定后端;这里使用了一个 Textifier):

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    if (debug && mv != null && ...){ // 如果必须跟踪此方法
        Printer p = new Textifier(ASM4) {
            @Override
            public void visitMethodEnd() {
                print(aPrintWriter); // 在其被访问后输出它
            }
        };
        mv = new TraceMethodVisitor(mv, p);
    }
    return new MyMethodAdapter(mv);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这一代码输出该方法经 MyMethodAdapter 转换过后的结果。

  1. CheckClassAdapter

这个类也已经在上一章介绍过,它检查 ClassVisitor 方法的调用顺序是否适当,参数是否有效,所做的工作与 MethodVisitor 方法相同。因此,可用于检查 MethodVisitor API 在一个转换链中任意点的使用是否正常。和 TraceMethodVisitor 类似, 可以用CheckMethodAdapter 类来检查一个方法,而不是检查它的整个类:

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    if (debug && mv != null && ...){ // 如果必须检查这个方法
        mv = new CheckMethodAdapter(mv);
    }
    return new MyMethodAdapter(mv);
}    
1
2
3
4
5
6
7

这一代码验证 MyMethodAdapter 正确地使用了 MethodVisitor API。但要注意,这一适配器并没有验证字节代码是正确的:例如,它没有检测出 ISTORE 1 ALOAD 1 是无效的。实际上,如果使用 CheckMethodAdapter 的其他构造器(见 Javadoc),并且在 visitMaxs中提供有效的 maxStack 和 maxLocals 参数,那这种错误是可以被检测出来的。

  1. ASMifier 这个类已经在上一章介绍过,也用于处理方法的内容。利用它,可以知道如何用 ASM 生成一些编译后的代码:只需要用 Java 编写相应的源代码,用 javac 编译它,然后用 ASMifier 访问这个类。你会得到 ASM 代码,以生成与源代码相对应的字节代码。

# 3.3.2 AnalyzerAdapter

这个方法适配器根据 visitFrame 中访问的帧,计算每条指令之前的栈映射帧。实际上, 如 3.1.5 节中的解释,visitFrame 仅在方法中的一些特定指令前调用,一方面是为了节省空间, 另一方面也是因为“其他帧可以轻松快速地由这些帧推导得出”。这就是这个适配器所做的工作。当然,它仅对那些包含预计算栈映射帧的类有效,也就是对于用 Java 6 或更高版本编译的有效(或者用一个使用 COMPUTE_FRAMES 选项的 ASM 适配器升级到 Java 6)。

在我们的 AddTimerAdapter 示例中,这个适配器可用于获得操作数栈恰在 RETURN 指令之前的大小,从而允许为 visitMaxs 中的 maxStack 计算一个最优的已转换值(事实上,在实践中并不建议使用这一方法,因为它的效率要远低于使用 COMPUTE_MAXS):

class AddTimerMethodAdapter2 extends AnalyzerAdapter {
    private int maxStack;

    public AddTimerMethodAdapter2(String owner, int access, String name, String desc, MethodVisitor mv) {
        super(ASM4, owner, access, name, desc, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
        mv.visitInsn(LSUB);
        mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
        maxStack = 4;
    }

    @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");
            maxStack = Math.max(maxStack, stack.size() + 4);
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
    }
}   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

stack 字段在 AnalyzerAdapter 类中定义,包含操作数栈中的类型。更准确地说,在一个 visitXxx Insn 中,且在调用被重写的方法之前,它会列出操作数栈正好在这条指令之前的状态。注意,必须调用被重写的方法,使 stack 字段被正确更新(因此,用 super 代替源代码中的 mv)。

或者,也可以通过调用超类中的方法来插入新指令:其方法就是这些指令的帧将由 AnalyzerAdapter 计算,由于这个适配器会根据它计算的帧来更新 visitMaxs 的参数,所以我们不需要自己来更新它们:

class AddTimerMethodAdapter3 extends AnalyzerAdapter {
    public AddTimerMethodAdapter3(String owner, int access,
                                  String name, String desc, MethodVisitor mv) {
        super(ASM4, owner, access, name, desc, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        super.visitFieldInsn(GETSTATIC, owner, "timer", "J");
        super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
        super.visitInsn(LSUB);
        super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            super.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            super.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            super.visitInsn(LADD);
            super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
        }
        super.visitInsn(opcode);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 3.3.3 LocalVariablesSorter

这个方法适配器将一个方法中使用的局部变量按照它们在这个方法中的出现顺序重新进行编号。例如,在一个有两个参数的方法中,第一个被读取或写入且索引大于或等于 3 的局部变量 (前三个局部变量对应于 this 及两个方法参数,因此不会发生变化)被赋予索引 3,第二个被赋予索引 4,以此类推。在向一个方法中插入新的局部变量时,这个适配器很有用。没有这个适配 器,就需要在所有已有局部变量之后添加新的局部变量,但遗憾的是,在 visitMaxs 中,要直到方法的末尾处才能知道这些局部变量的编号。

为说明如何使用这个适配器,假定我们希望使用一个局部变量来实现 AddTimerAdapter:

public class C {
    public static long timer;

    public void m() throws Exception {
        long t = System.currentTimeMillis();
        Thread.sleep(100);
        timer += System.currentTimeMillis() - t;
    }
}
1
2
3
4
5
6
7
8
9

这一点很容易做到:只需扩展 LocalVariablesSorter ,并使用这个类中定义的 newLocal 方法。

class AddTimerMethodAdapter4 extends LocalVariablesSorter {
    private int time;

    public AddTimerMethodAdapter4(int access, String desc, MethodVisitor mv) {
        super(ASM4, access, desc, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
        time = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, time);
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            mv.visitVarInsn(LLOAD, time);
            mv.visitInsn(LSUB);
            mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            mv.visitInsn(LADD);
            mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

注意,在对局部变量重新编号后,与该方法相关联的原帧变为无效,在插入新局部变量后更不必说了。幸好,还是可能避免从头重新计算这些帧的:事实上,并不存在必须添加或删除的帧, 只需对原帧中局部变量的内容进行重新排序, 为转换后的方法获得帧就“足够” 了。 LocalVariablesSorter 会自动负责完成。如果还需要为你的方法适配器进行增量栈映射帧更新,可以由这个类的源代码中获得灵感。

前面曾经说过,这个类的原版本中存在关于最糟情景下 maxStack 取值的问题,在上面可以看出,使用局部变量并不能解决这个问题。如果希望用 AnalyzerAdapter 解决这个问题, 除了 LocalVariablesSorter 之外,必须通过委托使用这些适配器,而不是通过继承(因为不可能存在多个继承):

class AddTimerMethodAdapter5 extends MethodVisitor {
    public LocalVariablesSorter lvs;
    public AnalyzerAdapter aa;
    private int time;
    private int maxStack;

    public AddTimerMethodAdapter5(MethodVisitor mv) {
        super(ASM4, mv);
    }

    @Override
    public void visitCode() {
        mv.visitCode();
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
        time = lvs.newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, time);
        maxStack = 4;
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            mv.visitVarInsn(LLOAD, time);
            mv.visitInsn(LSUB);
            mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            mv.visitInsn(LADD);
            mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            maxStack = Math.max(aa.stack.size() + 4, maxStack);
        }
        mv.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        mv.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

为使用这 个适配器 ,必须将 一个 LocalVariablesSorter 链接到一 个 AnalyzerAdapter,再将它自身连接到你的适配器:第一个适配器将对局部变量排序,并相应地更新帧,分析适配器将计算中间帧,在此过程中会考虑上一个适配器中完成的重新编号,你的适配器将可以访问这些重新编号的中间帧。这个链接可以在 visitMethod 中构造如下:

mv=cv.visitMethod(access,name,desc,signature,exceptions);
if(!isInterface && mv!=null && !name.equals("<init>")){
    AddTimerMethodAdapter5 at = new AddTimerMethodAdapter5(mv);
    at.aa = new AnalyzerAdapter(owner,access,name,desc,at);
    at.lvs = new LocalVariablesSorter(access,desc,at.aa);
    return at.lvs;
}
1
2
3
4
5
6
7

# 3.3.4 AdviceAdapter

这个方法适配器是一个抽象类,可用于在一个方法的开头以及恰在任意 RETURN 或 ATHROW 指令之前插入代码。它的主要好处就是对于构造器也是有效的,在构造器中,不能将代码恰好插入到构造器的开头,而是插在对超构造器的调用之后。事实上,这个适配器的大多数代码都专门用于检测对这个超构造器的调用。

仔细研究 3.2.4 节中的 AddTimerAdapter 类将会看到,AddTimerMethodAdapter 因为这一原因而未被用于构造器。这一方法适配器从 AdviceAdapter 继承而来,可以对其进行改 进,以便对于构造器同样有效(注意,AdviceAdapter 继承自 LocalVariablesSorter, 所以也可以轻松使用一个局部变量):

class AddTimerMethodAdapter6 extends AdviceAdapter {
    public AddTimerMethodAdapter6(int access, String name, String desc, MethodVisitor mv) {
        super(ASM4, mv, access, name, desc);
    }

    @Override
    protected void onMethodEnter() {
        mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                "currentTimeMillis", "()J");
        mv.visitInsn(LSUB);
        mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
    }

    @Override
    protected void onMethodExit(int opcode) {
        mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                "currentTimeMillis", "()J");
        mv.visitInsn(LADD);
        mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28