ASM-方法-工具类
‘org.objectweb.asm.commons’包含了一些预定义的方法适配器,可以用来定义自己的适配器。 本节介绍三个工具类,并且会展示它们如何和3.2.4节中的‘AddTimerAdapter’示例结合使用。 也展示了如何使用上一章中的工具简化方法的生成和转化。
在2.3章中介绍的工具也可以使用在方法操作上。
许多字节码指令,例如‘xLOAD‘、 ‘xADD’或者‘xRETURN’,取决于它们所应用的类型。 ‘Type’类提供了一个‘getOpcode’方法,对于这些指令,可以用来获取操作码所对应的给定类型。 这些方法需要一个整数型的操作码作为参数,调用后返回一个对应该类型的操作码。 例如‘t.getOpcode(IMUL)’,如果t是‘Type.FLOAT_TYPE’的话,就会返回‘FMUL’。
这个类,在上一章已经介绍过了,以文本型的描述打印它所访问的类,包括文本型描述该类的方法,和本章中使用的方式非常类似。 因此可以用于在转换链中的任何一点上,跟踪生成或者转换方法的内容。例如:
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.TraceClassVisitor \
java.lang.Void
打印:
// 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’):
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
// if this method must be traced
if (debug && mv != null &&...){
Printer p = new Textifier(ASM4) {
@Override
public void visitMethodEnd() {
// print it after it has been visited
print(aPrintWriter);
}
};
mv = new TraceMethodVisitor(mv, p);
}
return new MyMethodAdapter(mv);
}
这段代码将会打印经过‘MyMethodAdapter’转换的方法。
这个类已经在上一章中介绍过了,用于检查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 this method must be checked
if (debug && mv != null && ...) {
mv = new CheckMethodAdapter(mv);
}
return new MyMethodAdapter (mv);
}
这段代码检测MyMethodAdapter类使用‘MethodVisitor API’是否正确。 需要注意的是,这个是配器不会检测字节码是否争取:例如它不会见从‘ISTORE 1 ALOAD 1’是否正确。 事实上,这类错误是可以被检测到的,如果你使用CheckMethodAdapter其他的构造函数(参考Javadoc), 并且如果你为visitMaxs方法提供合法的方maxStack和maxLocals**参数。
这个类在上一章中介绍过,也可以用于方法的内容。 它可以被用来知道如果通过ASM生成某些编译后的代码:在Java中只要编写相应的源码即可,使用javac编译,并且使用ASMifier访问编译后的类。 你将会得到ASM代码,来生成相应源码的字节码。
这个方法适配器会根据visitFrame方法中被访问的帧,计算出每一个指令之前的栈哈希帧。 事实上,如前面3.1.5节所描述的,为了节省空间,visitFrame仅仅会在一个方法中某些特定的指令前调用,并且“其他的帧也可以从这些帧简单容易的推算出来”。 这就是AnalyzerAdapter的作用。当然在它仅能作用在包含了预先计算过栈哈希帧的编译类,即使用Java 6或者更改版本编译的类(或者像之前的示例一样,使用含有COMPUTE_FRAMES参数的ASM adapter将类升级到Java 6):
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方法的参数,我们就不必更新这些参数了:
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(索引值是从0开始的),前三个本地变量对应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;
}
}
这个用继承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);
}
}
注意,当本地变量重写编号后,该方法关联的原始帧就都失效了,更不用说插入新的本地变量了。 庆幸的是避免重头计算这些帧是可行的:事实上没有任何帧需要被添加或者删除,在原本的帧上有足够的空间可以重新排列转换后方法的本地变量。 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);
}
}
为了使用这个适配器,你必须将一个LocalVariablesSorter连接到AnalyzerAdapter,它自身则链接到自定义的适配器上:因此第一个适配器会排列本地变量并且更新帧,考虑到上一章中的重编号,analyzer adapter会计算中间帧,自定义的适配器可以访问到中间帧中重排序的编号。 在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;
}
这个方法适配器是一个抽象类,可以用于在方法的开始和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);
}
}