ASM-方法-工具类
‘org.objectweb.asm.commons’包含了一些预定义的方法适配器,可以用来定义自己的适配器。
本节介绍三个工具类,并且会展示它们如何和3.2.4节中的‘AddTimerAdapter’示例结合使用。
也展示了如何使用上一章中的工具简化方法的生成和转化。
在2.3章中介绍的工具也可以使用在方法操作上。
Type
许多字节码指令,例如‘xLOAD‘、 ‘xADD’或者‘xRETURN’,取决于它们所应用的类型。
‘Type’类提供了一个‘getOpcode’方法,对于这些指令,可以用来获取操作码所对应的给定类型。
这些方法需要一个整数型的操作码作为参数,调用后返回一个对应该类型的操作码。
例如‘t.getOpcode(IMUL)’,如果t是‘Type.FLOAT_TYPE’的话,就会返回‘FMUL’。
TraceClassVisitor
这个类,在上一章已经介绍过了,以文本型的描述打印它所访问的类,包括文本型描述该类的方法,和本章中使用的方式非常类似。
因此可以用于在转换链中的任何一点上,跟踪生成或者转换方法的内容。例如:
1 2 3
| java -classpath asm.jar:asm-util.jar \ org.objectweb.asm.util.TraceClassVisitor \ java.lang.Void
|
打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| // 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 }
|
这里展示类如何生成一个静态代码块‘static { … }’,使用命名为‘‘的方法(对应类的初始化)。
注意,如果你想跟踪链路某一点上单个方法,而不是跟踪该类的全部内容,你可以使用‘TraceMethodVisitor’类代替‘TraceClassVisitor’(在这种情况下,你必须明确指定后端;在这里我们使用‘Textifier’):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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); }
|
这段代码将会打印经过‘MyMethodAdapter’转换的方法。
CheckClassAdapter
这个类已经在上一章中介绍过了,用于检查ClassVisitor方法是否按照适当顺序被调用、方法参数是否合法,它也同样适用于MethodVisitor的方法。
因此它可以被用于转换链路中的任意一点,用于检测MethodVisitor API是否被正确使用。
和TraceMethodVisitor一样,你可以使用CheckMethodAdapter检测单个方法而不是整个类:
1 2 3 4 5 6 7 8 9 10
| 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); }
|
这段代码检测MyMethodAdapter类使用‘MethodVisitor API’是否正确。
需要注意的是,这个是配器不会检测字节码是否争取:例如它不会见从‘ISTORE 1 ALOAD 1’是否正确。
事实上,这类错误是可以被检测到的,如果你使用CheckMethodAdapter其他的构造函数(参考Javadoc),
并且如果你为visitMaxs方法提供合法的方maxStack和maxLocals**参数。
ASMifier
这个类在上一章中介绍过,也可以用于方法的内容。
它可以被用来知道如果通过ASM生成某些编译后的代码:在Java中只要编写相应的源码即可,使用javac编译,并且使用ASMifier访问编译后的类。
你将会得到ASM代码,来生成相应源码的字节码。
3.3.2. AnalyzerAdapter
这个方法适配器会根据visitFrame方法中被访问的帧,计算出每一个指令之前的栈哈希帧。
事实上,如前面3.1.5节所描述的,为了节省空间,visitFrame仅仅会在一个方法中某些特定的指令前调用,并且“其他的帧也可以从这些帧简单容易的推算出来”。
这就是AnalyzerAdapter的作用。当然在它仅能作用在包含了预先计算过栈哈希帧的编译类,即使用Java 6或者更改版本编译的类(或者像之前的示例一样,使用含有COMPUTE_FRAMES参数的ASM adapter将类升级到Java 6):
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
| 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); } }
|
‘stack’属性在AnalyzerAdapter类中有定义,并且包含了在操作栈中的类型。
更确切地说,对于一个‘visitXxx Insn’指令,在覆盖方法被调用前,这个列表包含了在该条指令前操作栈的状态。
需要注意的是覆盖方法必须被调用,这样栈里的属性才能正确地更新(因此使用父类的原始方法,而不是mv的方法)。
另外,调用父类的方法也可以插入新指令:效果是AyalyzerAdapter会计算出这些指令对应的帧。
因此,该适配器会基于它计算出的帧更新visitMaxs方法的参数,我们就不必更新这些参数了:
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
| 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); } }
|
3.3.3 局部变量排序器 LocalVariablesSorter
这个方法适配器会根据本地变量出现的顺序重新排列本地变量。
例如一个包含两个参数的方法,第一个局部变量读写的索引大于等于3(索引值是从0开始的),前三个本地变量对应this和两个方法参数(这是不可改变的),第一个变量索引值是3,第二个是4,依此类推。
这个适配器对于插入新的本地变量很有帮助。
如果没有这个适配器,就必须在所有本地变量之后插入新的本地变量,但不幸的是这些变量的编号在方法结束前(即visitMaxs方法前)是不知道的。
为了展示这个适配器是如何被使用的,我们假设想使用一个本地变量实现AddTimerAdapter:
1 2 3 4 5 6 7 8
| public class C { public static long timer; public void m() throws Exception { long t = System.currentTimeMillis(); Thread.sleep(100); timer += System.currentTimeMillis() - t; } }
|
这个用继承LocalVariablesSorter类的方式可以很容易实现,通过使用该类中的newLocal方法:
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
| 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); } }
|
注意,当本地变量重写编号后,该方法关联的原始帧就都失效了,更不用说插入新的本地变量了。
庆幸的是避免重头计算这些帧是可行的:事实上没有任何帧需要被添加或者删除,在原本的帧上有足够的空间可以重新排列转换后方法的本地变量。
LocalVariablesSorter会自动处理这些。
如果需要为一个新的方法适配器增加栈哈希帧,你可以从这个类的源码获得提示。
如上所见,在该类的原始版本中,考虑到maxStack的最坏情况,使用一个本地变量不能解决这个问题。
如果你想使用AnalyzerAdapter来解决这个问题,除了LocalVariablesSorter,你必须通过委托机制使用这些适配器,而不是继承(因为多继承是不被允许的):
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
| 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); } }
|
为了使用这个适配器,你必须将一个LocalVariablesSorter连接到AnalyzerAdapter,它自身则链接到自定义的适配器上:因此第一个适配器会排列本地变量并且更新帧,考虑到上一章中的重编号,analyzer adapter会计算中间帧,自定义的适配器可以访问到中间帧中重排序的编号。
在visitMethod方法中,这个链路可以被构造成如下所示:
1 2 3 4 5 6 7
| 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; }
|
3.3.4 AdviceAdapter
这个方法适配器是一个抽象类,可以用于在方法的开始和RETURN、ATHROW指令前插入代码。
它的主要优点是,也可以对构造函数起作用,代码不能在构造函数的一开始就插入,要在调用父类的构造函数后插入。
事实上,本节的大部分代码都是为了检测父类的构造函数而调用的。
如果仔细研究3.2.4节中的AddTimerAdapter类,你会发现因为这个原因,AddTimerMethodAdapter没有用于构造函数。
通过继承AdviceAdapter这个方法适配器,改进后也可以适用构造函数(注意,AdviceAdapter继承了LocalVariablesSorter,因此我们可以非常方便的使用一个本地变量):
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
| 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); } }
|