3.2 ASM-方法-接口和组件

时间:2022-06-06
本文章向大家介绍3.2 ASM-方法-接口和组件,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

ASM-方法-接口和组件

3.2 接口和组件

3.2.1 介绍

在ASM API中,用来生成和转变编译后方法的都是基于‘MethodVisitor’抽象类的(参照图表 3.4),这是由‘ClassVisitor’‘visitMethod’方法返回的。 除了一些注解和调试相关的信息(这些信息将在下一章说明),这个类定义了每个字节码指令类别一个方法,根据这些指令的参数数量和参数类型(这些类别不对应3.1.2节介绍的那些类别)。 这些方法必须按照以下顺序调用(和MethodVisitor接口在Javadoc中指定的一些额外约束):

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxx Insn | visitLocalVariable | visitLineNumber ) *
visitMaxs )?
visitEnd

这意味着,如有注释和属性的话,则必须先访问,后面是非抽象方法的字节码。 对于这些方法,这些代码必须按顺序访问,在唯一一个‘visitCode’方法调用和唯一一个‘visitMaxs’方法调用之间。

代码 3.4.: MethodVisitor类 abstract class MethodVisitor { // public accessors ommited MethodVisitor(int api); MethodVisitor(int api, MethodVisitor mv); 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 visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs); 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(); }

因此在一系列的事件中,‘visitCode’方法和‘visitMaxs’方法可以用于检测一个方法字节码的开始和结束。 和class一样,‘visitEnd’方法必须最后调用,并且用于检测在一系列事件中一个方法的结束。

‘ClassVisitor’‘MethodVisitor’类整合后可以用于生成一个完整的类:

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 API的核心组件,用于生成和转换方法:

  1. ClassReader类解析一个编译后的方法,并且通过传递ClassVisitor作为accept方法的参数获得的返回,调用MethodVisitor’相应的方法。
  2. ClassWriter‘visitMethod’返回了MethodVisitor抽象类的一个实现,该实现可以直接用二进制的方式构建编译后的方法。
  3. MethodVisitor类可以传递所有调用它的方法给另一个MethodVisitor类。MethodVisitor类可以看作一个事件过滤器。

第50页 50/154

ClassWriter选项 如3.1.5节所讲,计算一个方法的栈哈希帧不是一件简单的事情:你需要计算所有的帧,找到对应着跳转目标的帧,或者在无条件跳转后紧挨着的帧,并且最后还要压缩保留的帧。 同样的,计算本地变量和操作栈部分的大小看似容易,其实不然。 庆幸的是ASM可以帮你处理这些。 当你创建一个ClassWriter对象的时候,你可以指定自动计算这些:

  • 使用‘new ClassWriter(0)’构造函数,不会自动计算这些属性。你必须自己计算帧的大小、本地变量的大小和操作栈的大小。
  • 使用‘new ClassWriter(ClassWriter.COMPUTE_MAXS)’构造函数,本地变量的大小和操作栈的大小会被自动计算。你仍然需要调用‘visitMax’方法,但是你可以传递任意参数:ASM会忽略这些参数,并重新计算它们的大小。使用该选项,你必须计算帧的大小。
  • 使用‘new ClassWriter(ClassWriter.COMPUTE_FRAMES)’构造函数,所有的属性都会被自动计算。你不必调用‘visitFrame’方法,但你仍然需要调用‘visitMax’方法(ASM会忽略这些参数,并重新计算它们的大小)。

使用这些选项非常方便,但有一定的性能损耗:‘COMPUTE_MAX’会使‘ClassWriter’慢10%,‘COMPUTE_FRAMES’选项会慢两倍。 这必须将程序自己计算的时间与开发人员自己计算的时间相比较:在特定的情况下往往有更方便、快速的算法来计算这些,跟在ASM中使用的算法相比,必须能够处理所有情况。

需要注意的是,如果开发人员自己计算帧的大小,可以让‘ClassWriter’来为你处理压算环节。 这种情况下,你紧紧需要调用‘visitFrame(F_NEW, nLocals, locals, nStack, stack)’去访问未被压缩的帧,‘nLocals’**和‘nStack’表示本地变量和操作栈的大小,*’locals’‘stack’*是这些值相对应的数据类型数组(详情参考Javadoc)。

同样需要注意的是,为了自动计算帧,有时候需要还要计算两个给定class的公共的超类。 默认‘ClassWriter’会为它们计算,在‘getCommonSuperClass’方法中,通过加载两个class到JVM中,并通过反射API实现。 如果你生成了几个class文件,并且它们之间相互引用,这可能是会是一个问题,因为引用的class可能还不存在。 在这种情况下,你可以重写‘getCommonSuperClass’方法来解决这个问题。

3.2.2 生成方法 Generating methods

在3.1.3章中定义的‘getF’方法的字节码,可以通过下面的方法调用生成(mv是一个MethodVisitor对象):

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节所见,本地变量区和操作栈的大小都是一个槽。 最后调用‘visitEnd’方法生成本方法。

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

‘visitCode’‘visitEnd’方法调用之间,你可以看到与3.1.5节结束时展示的字节码所准确对应的方法调用:每个指令、标签和帧都对应一个方法调用(唯一的例外就是声明和构造‘labe’‘end’标签)。

备注

一个标签对象用于后面的需要该标签的指令。 例如‘end’标签用于‘RETURN’指令,并不是紧跟其后的被访问的帧,因为帧不是一个指令。 多个标签用于一个相同的指令是完全合法的,但一个标签只能仅仅用于一个指令。 换句话说,使用不同的标签连续调用‘visitLabel’方法是可以的,但在一个指令中的标签必须仅能被‘visitLabel’方法调用一次。 最后一个约束是标签是不能被共享的:每个方法必须有他们自己的标签。

3.2.3. 改造方法 Transforming methods

现在可以猜到,可以像改造class一样改造方法,即通过使用一个方法适配器转发调用它的方法,并进行一些修改:通过改变参数可以用于修改单条指令,通过不转发收到的调用来移除一个指令,通过在收到的调用间插入方法来插入新的指令。 ‘MethodVisitor’类提供了一个这样的方法适配器,不做任何处理,仅仅转发它所收到的方法调用。

为了理解如何使用方法适配器,让我们来设计一个非常简单的适配器:移除方法内部的‘NOP’指令(移除这些指令不会出现任何问题,因为它们不做任何操作):

import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.NOP;

import org.objectweb.asm.MethodVisitor;

public class RemoveNopAdapter extends MethodVisitor {
    public RemoveNopAdapter(MethodVisitor mv) {
        super(ASM4, mv);
    }
    @Override
    public void visitInsn(int opcode) {
        if (opcode != NOP) {
            mv.visitInsn(opcode);
        }
    }
}

该适配器可以用在一个class适配器中,如下所示:

import static org.objectweb.asm.Opcodes.ASM4;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

public class RemoveNopClassAdapter extends ClassVisitor {
    public RemoveNopClassAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name,
        String desc, String signature, String[] exceptions) {
        MethodVisitor mv;
        mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null) {
            mv = new RemoveNopAdapter(mv);
        }
        return mv;
    }
}

换句话说,在链路中,类适配器(ClassAdapter)仅仅创建了一个方法适配器(MethodAdapter),封装了下一个类访问器(ClassVisitor)返回的方法访问器(MethodVisitor),并返回封装后的方法适配器。 其效果是一个方法适配链的构建,似于一个类适配链的构建(参考图表 3.5)。

但请注意,这并非是强制:可以完全构建一个不同于类适配链的方法适配链。 甚至每一个方法都有一个不同的方法适配链。 例如,类适配器可以选择只移除普通方法中的‘NOP’指令,而不移除构造函数中的。 实现的方式如下:

...
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null && !name.equals("<init>")) {
  mv = new RemoveNopAdapter(mv);
}
...

在这种情况下,适配链对于构造函数来说会短一点。 与之相反,构造函数的调用链也可以更长,当几个方法适配链在visitMethod方法中被一起创建。 甚至方法适配链可以于类适配链拥有不同的拓扑结构。 例如类适配链可以是线性的,方法适配链可以具有多个分支结构:

public MethodVisitor visitMethod(int access, String name,
    String desc, String signature, String[] exceptions) {
  MethodVisitor mv1, mv2;
  mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
  mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
  return new MultiMethodAdapter(mv1, mv2);
}

现在,你已经知道在一个类适配器中,多个方法适配如何使用和结合,接下来让我们实现一些比‘RemoveNopAdapter’更加有趣的适配器。

3.2.4 无状态转换 Stateless transformations

假设我们要检测一个程序中每个类的耗时,我们需要在每一个类中加入一个静态的timer属性,我们只需要把每一个方法的执行时间加在该属性上。 换句话说,我们想要将一个类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’输出两个编译类的字节码,并进行比较(可以使用Textifier,或者使用ASMifier)。 下面是使用默认的Textifier输出,注意不同的部分(粗体):

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

可以看到,我们需要在方法的开始增加四条指令,在‘return’指令前增加另外四条指令。 同样也需要更改操作栈的最大值。方法代码的开始使用visitCode方法访问。 因此我们可以在方法适配器中重写该方法,增加开始的四条指令:

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)’,即取变量‘s’和常量‘4’的最大值。 不幸的,我们还在方法结束前增加了四条指令,我们需要知道在这四条指令执行前,操作栈的大小。 我们只知道,该值小于等于‘s’。因此我们可以总结出来,插入这四条指令后,我们需要将栈的空间大小设置成‘s+4’。 这种最坏的情况在实践中几乎和很少发生:通常的编译器,在执行返回指令前操作栈中仅保留返回值,即需要的栈空间大小可能是0、1,最多是2。 但是如果我们想要处理所有可能发生的情况,我们就必须作最坏的打算2。

2:最坏的情况 给出最佳的操作栈不是必要的。给出任何大于等于最佳操作栈大小的值就可以,尽快这样会浪费线程执行栈的内存。

我们必须重写‘visitMaxs’方法,如下所示:

public void visitMaxs(int maxStack, int maxLocals) {
  mv.visitMaxs(maxStack + 4, maxLocals);
}

当然也可以不用操心最大操作栈大小,可以依靠‘COMPUTE_MAXS’参数,使用该参数后会计算出最佳的操作栈大小,而不是最坏情况的值。 对于这么一个简单的转换,没有必要花费大力气来手动更新‘maxStack’

现在一个有趣的问题是:栈哈希帧怎么办? 源码中不包含任何帧,也没有任何相转换的代码,但这是由于我们代码使用了特殊的代码么? 是否在某些特定的情境下这些帧会被更新? 答案是否定的,因为:

  1. 插入的指令离不开操作栈的变化。
  2. 插入的代码不能包含跳转指令。
  3. 跳转指令,更确切的讲,源码的控制流图没有改变。 这说明原始的帧没有变化,由于插入的新代码没有新增帧,压缩的原始帧也不需要修改。

现在我们可以把所有的元素组合到一起,并于‘ClassVisitor’‘MethodVisitor的子类相关联:

public class AddTimerAdapter extends ClassVisitor {
  private String owner;
  private boolean isInterface;

  public AddTimerAdapter(ClassVisitor cv) {
    super(ASM4, 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 MethodVisitor {
    public AddTimerMethodAdapter(MethodVisitor mv) {
      super(ASM4, 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);
    }
  }
}

类适配器用于实例化方法适配器(排除构造方法),但也在类中加入了一个timer属性,通过存储在类中的该属性名,方法适配器可以访问该属性。

3.2.5. 有状态转换 Statefull transformations

在上一节中的讲述的转换是本地的,并不依赖在当前转换前已经访问的指令集:比如在方法开始时插入的代码都是相同的,与之类似的,代码在每个‘RETURN’指令前插入的,也都是相同。 像这样的转换被成为无状态的转换。这种转换很容易使先,只有简单的转换并验证该属性。 更负责的转换需要在本次转换前,存储某些已被访问过的指令的状态。 例如,考虑一个转换:移除所有出现的指令序列‘ICONST_0 IADD’,该指令序列产生的效果是数值加0。 显然,当一个‘IADD’指令被访问时,当且仅当上一个被访问的指令是‘ICONST_0’时,移除这两个指令。

这个转换需要在方法适配器中存储状态信息。 出于这个原因,这种转换被称为有状态转换。 让我们更加仔细的看一下这个例子。当一个‘ICONST_0’指令被访问时,如果下一条指令是IADD,那么该条指令必须被移除。 问题是下一条指令还不知道是什么。 解决方法是推迟访问该指令到一下条指令:当访问到下一个指令时,如果指令是‘IADD’,那么移除这两个指令,否则访问‘ICONST_0’指令和当前指令。 为了实现删除或替换某些指令序列,可以很方便的引入一个‘MethodVisitor’的子类,使用子类的‘visitXxx Insn’方法调用通用的‘visitInsn’方法:

public abstract class PatternMethodAdapter extends MethodVisitor {
  protected final static int SEEN_NOTHING = 0;
  protected int state;

  public PatternMethodAdapter(int api, MethodVisitor mv) {
    super(api, mv);
  }

  @Overrid
  public void visitInsn(int opcode) {
    visitInsn();
    mv.visitInsn(opcode);
  }

  @Override
  public void visitIntInsn(int opcode, int operand) {
    visitInsn();
    mv.visitIntInsn(opcode, operand);
  }

  ...

  protected abstract void visitInsn();
}

然后,上面的转换可以被这样实现:

public class RemoveAddZeroAdapter extends PatternMethodAdapter {
  private static int SEEN_ICONST_0 = 1;

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

  @Override
  public void visitInsn(int opcode) {
    if (state == SEEN_ICONST_0) {
      if (opcode == IADD) {
        state = SEEN_NOTHING;
        return;
      }
    }
    visitInsn();
    if (opcode == ICONST_0) {
      state = SEEN_ICONST_0;
      return;
    }
    mv.visitInsn(opcode);
  }

  @Override
  protected void visitInsn() {
    if (state == SEEN_ICONST_0) {
      mv.visitInsn(ICONST_0);
    }
    state = SEEN_NOTHING;
  }
}

‘visitInsn(int)’方法首先测试需要移除的指令序列是否被检测到了。 在这种情况中,重新初始化状态后就立即返回,产生的效果是移除指令集序列。 在其他情况下,调用通用的‘visitInsn(int)’方法,发出一个‘ICONST_0’指令,如果这个是最后被访问的指令。 然后,如果当前的指令是‘ICONST_0’,就会记录这种状态并且返回,为了推迟关于该指令的决策。 在所有其他情况下,当前指令被转发给下一个访问器。

标签和帧:Labels and frames

在上一节中,我们看到标签和帧会在他们所关联的指令前即将访问前被访问。 换句话说他们作为指令集同时被访问,尽管他们本身不是指令。 这会对检测指令集序列的转换产生影响,但这种影响实际上是一个优点。 事实上,如果我们移除的一个指令是一个跳转指令的目标,那么会发生什么? 如果某些指令可能会跳转到‘ICONST_0’,这意味着有一个标签指派该指令。 在移除这两个指令后,这个标签会指派给已被移除‘IADD’指令的后一个指令,这是我们想要的。 但某些指令可能跳转到‘IADD’指令,我们就不能移除这个指令序列(我们不能保证,在跳转指令前有一个‘0’值被压入到栈上)。。 庆幸的是,在本示例中,在‘ICONST_0’指令和‘IADD’指令中间肯定会有一个标签,可以很容易检测到。

原因和栈哈希帧相同:如果在两个指令中间有一个栈哈希帧被访问,我们就不能移除这两个指令。 在这两种情况下,可以在模式匹配算法中,将标签和帧作为指令处理。 这些可以在‘PatternMethodAdapter’中实现(注意:visitMaxs也会调用通用的‘visitInsn’方法,用于处理将一个方法的结束当作序列的前缀,这种情况必须被检测到):

public abstract class PatternMethodAdapter extends MethodVisitor {
  ...
  @Override public void visitFrame(int type, int nLocal, Object[] local,
    int nStack, Object[] stack) {
    visitInsn();
    mv.visitFrame(type, nLocal, local, nStack, stack);
  }
  @Override public void visitLabel(Label label) {
    visitInsn();
    mv.visitLabel(label);
  }
  @Override public void visitMaxs(int maxStack, int maxLocals) {
    visitInsn();
    mv.visitMaxs(maxStack, maxLocals);
  }
}

在下一节我们会看到,一个编译后的方法可能会包含源文件的行号,用于实例化异常的栈跟踪信息。 这种信息使用‘visitLineNumber’方法访问,和调用指令同时掉用。 在两个指令序列的中间仍然存在行号,这对转换和移除指令序列没有任何影响。 因此解决方案是在匹配算法中完全忽视他们。

一个更加复杂的示例

上一个示例可以很容易的延用到更加复杂的指令序列。 设想一个转换,移除自己赋值给自己的指令,一般是由错误书写,比如‘f==f’;或者‘ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f’这样的字节码。 在实现这个转换之前,最好先设计一个状态机来识别这种序列(参考图表 3.6)。

每个过渡都标有一个条件(当前指令的值)和一个动作(一个必须被发送的指令序列)。 例如,如果当前的指令不是‘ALOAD 0’会发生从‘S1’‘S0’的过渡。 在这种情况下访问‘ALOAD 0’指令,能到达该状态,会被发送。 需要注意‘S2’到本身的过度:这个过度发生在有三个以上的连续‘ALOAD 0’指令被访问,就将两个‘ALOAD 0’之后访问的‘ALOAD 0’指令发送出去。 只要状态机被发现了,编写相应的方法适配器是很简单的(8个swtich case对应了图表中的8种过渡):

class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {
  private final static int SEEN_ALOAD_0 = 1;
  private final static int SEEN_ALOAD_0ALOAD_0 = 2;
  private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
  private String fieldOwner;
  private String fieldName;
  private String fieldDesc;

  public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {
    super(mv);
  }

  @Override
  public void visitVarInsn(int opcode, int var) {
    switch (state) {
    case SEEN_NOTHING: // S0 -> S1
      if (opcode == ALOAD && var == 0) {
        state = SEEN_ALOAD_0;
        return;
      }
      break;
    case SEEN_ALOAD_0: // S1 -> S2
      if (opcode == ALOAD && var == 0) {
        state = SEEN_ALOAD_0ALOAD_0;
        return;
      }
      break;
    case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
      if (opcode == ALOAD && var == 0) {
        mv.visitVarInsn(ALOAD, 0);
        return;
      }
      break;
    }
    visitInsn();
    mv.visitVarInsn(opcode, var);
  }

  @Override
  public void visitFieldInsn(int opcode, String owner, String name, String desc) {
    switch (state) {
    case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
      if (opcode == GETFIELD) {
        state = SEEN_ALOAD_0ALOAD_0GETFIELD;
        fieldOwner = owner;
        fieldName = name;
        fieldDesc = desc;
        return;
      }
      break;
    case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
      if (opcode == PUTFIELD && name.equals(fieldName)) {
        state = SEEN_NOTHING;
        return;
      }
      break;
    }
    visitInsn();
    mv.visitFieldInsn(opcode, owner, name, desc);
  }

  @Override
  protected void visitInsn() {
    switch (state) {
    case SEEN_ALOAD_0: // S1 -> S0
      mv.visitVarInsn(ALOAD, 0);
      break;
    case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
      mv.visitVarInsn(ALOAD, 0);
      mv.visitVarInsn(ALOAD, 0);
      break;
    case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
      mv.visitVarInsn(ALOAD, 0);
      mv.visitVarInsn(ALOAD, 0);
      mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
      break;
    }
    state = SEEN_NOTHING;
  }
}

注意,出于和3.2.4节中‘AddTimerAdapter’示例中相同的原因,在本节中有状态的转换不需要改变栈哈希帧:改造后原本的帧仍然有效。 甚至我们不需要改变本地变量和操作栈的大小。最后必须要声明的是,有状态的转换不仅限于检测和改造指令序列。 很多其他类型的转换也是有状态的。在这种情况下,在下一节将介绍方法适配器。