在 try-finally 块中嵌入方法的现有代码 (2)

Embed the existing code of a method in a try-finally block (2)

前段时间,我在 Embed the existing code of a method in a try-finally block 中询问如何使用 ASM 将方法主体包装在 try-finally 块中。 解决方案是访问 visitCode() 中方法体开头的 try 块标签,并在 visitInsn() 中访问带有 return 操作码的指令时完成 try-finally 块.我知道如果方法没有 return 指令,解决方案将无法工作,如果方法总是出现异常,则该指令适用。

不过,我发现前一种解决方案有时也不适合使用 return 指令的方法。如果一个方法有多个 return 指令,它将不起作用。原因是它生成了无效的字节码,因为在方法的开头添加了一个try-finally块,但完成了多个try-finally块。

通常(但可能取决于 javac 编译器),字节码方法包含单个 return 指令,所有 return 路径都通过跳转到该指令结束。但是用Eclipse编译下面的代码会导致字节码有两条return指令:

public boolean isEven(int x) {
  return x % 2 == 0;
}

用Eclipse编译的字节码:

   0: iload_1
   1: iconst_2
   2: irem
   3: ifne          8
   6: iconst_1
   7: ireturn       // javac compilation: goto 9
   8: iconst_0
   9: ireturn

因此,我想知道包装方法代码的整个代码的正确方法是什么。

您必须回溯 Java 编译器在编译 try … finally … 时所做的工作,这意味着将您的 finally 操作复制到受保护(源)代码块将被保留的每个点(即 return 指令)并安装多个受保护(生成的字节码)区域(因为它们不应该覆盖您的 finally 操作)但它们可能都指向同一个异常处理程序。或者,您可以转换代码,用一个分支替换所有 return 指令到您的“之后”操作的一个实例,然后是唯一的 return 指令。

这不是微不足道的。因此,如果您不需要热代码替换,它通常不支持向已加载的 class 添加方法,避免这一切的最简单方法是将原始方法重命名为不与其他方法冲突的名称(您可能使用普通源代码中不允许的字符)并使用旧名称和签名创建一个新方法,该方法由一个简单的 try … finally … 结构组成,其中包含对重命名方法的调用。

例如将 public void desired() 更改为 private void desired$instrumented() 并添加一个新的

public void desired() {
    //some logging X

    try {
        desired$instrumented();
    }
    finally {
        //some logging Y
    }
}

请注意,由于调试信息保留在重命名的方法中,如果在重命名的方法中抛出异常,堆栈跟踪将继续报告正确的行号。如果你只是通过添加一个不可见的字符来重命名它(请记住你在字节码级别有更多的自由),它会非常顺利。

感谢 Holger 的回答和 Antimony 的评论,我开发了以下满足我需求的解决方案。 后来我发现在 Using ASM framework to implement common bytecode transformation patterns, E. Kuleshov, AOSD.07, March 2007, Vancouver, Canada 中也描述了类似的方法。

此解决方案不适用于不包含非异常return的方法(在每个执行路径中抛出异常的方法,例如throw new NotSupportedOperationException();)!

如果您也需要支持这些方法,您应该按照 Holger 的建议重命名原始方法,然后使用旧名称添加新方法。将添加的方法中的委托调用添加到重命名的方法中,并将调用嵌入到 try-finally 块中。


我使用简单的MethodVisitor访问代码。在 visitCode() 方法中,我添加了进入方法时要执行的指令。然后,我通过访问一个新的 Label 添加了 try 块的开头。当我访问 visitInsn() 中的 return 操作码时,我将完成 try 块并添加 finally 块。此外,我添加了一个新的 Label 来开始一个新的 try 块,以防该方法包含更多 return 指令。 (如果标签后面没有return说明,访问将没有任何效果。)

简化代码如下:

public abstract class AbstractTryFinallyMethodVisitor extends MethodVisitor {

  private Label m_currentBeginLabel;
  private boolean m_isInOriginalCode = true;

  protected void execBeforeMethodCode() {
    // Code at the beginning of the method and not in a try block
  }

  protected void execVisitTryBlockBegin() {
    // Code at the beginning of each try block
  }

  protected void execVisitFinallyBlock() {
    // Code in each finally block
  }

  @Override
  public void visitCode() {
    try {
      m_isInOriginalCode = false;
      execBeforeMethodCode();
      beginTryFinallyBlock();
    }
    finally {
      m_isInOriginalCode = true;
    }
  }

  protected void beginTryFinallyBlock() {
    m_currentBeginLabel = new Label();
    visitLabel(m_currentBeginLabel);
    execVisitTryBlockBegin();
  }

  @Override
  public void visitInsn(int opcode) {
    if (m_inOriginalCode && isReturnOpcode(opcode) {
      try {
        m_isInOriginalCode = false;
        completeTryFinallyBlock();

        super.visitInsn(opcode);

        beginTryBlock();
      }
      finally {
        m_isInOriginalCode = true;
      }
    }
    else {
      super.visitInsn(opcode);
    }
  }

  protected void completeTryFinallyBlock() {
    Label l1 = new Label();
    visitTryCatchBlock(m_currentBeginLabel, l1, l1, null);
    Label l2 = new Label();
    visitJumpInsn(GOTO, l2);
    visitLabel(l1);
    // visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] { "java/lang/Throwable" });
    visitVarInsn(ASTORE, 1);

    execVisitFinallyBlock();

    visitVarInsn(ALOAD, 1);
    super.visitInsn(ATHROW);
    visitLabel(l2);
    // visitFrame(Opcodes.F_SAME, 0, null, 0, null);

    execVisitFinallyBlock();
  }

   protected static boolean isReturnOpcode(int opcode) {
     return opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN;
   }
}

备注:

  • 如果您使用 COMPUTE_FRAMES 标志实例化 ClassWriter,则不应调用 visitFrame
  • 也可以(并且可能更可取)使用 AdviceAdapter 并在其 onMethodEnter()onMethodExit() 方法中执行字节码操作。
  • 如前所述,只有当字节码包含至少一条return指令时,才会添加try-finally块。

问题的 isEven() 方法的转换字节码输出将是:

public boolean isEven(int);
Code:
 0: ldc           #22                 //isEven(int)
 2: invokestatic  #28                 //log/Logger.push:(Ljava/lang/String;)V
 5: iload_1                  *1*
 6: iconst_2                 *1*  
 7: irem                     *1*
 8: ifne          25         *1*
11: iconst_1                 *1*
12: goto          21         *1*
15: astore_1
16: invokestatic  #31                 //log/Logger.pop:()V
19: aload_1            
20: athrow
21: invokestatic  #31                 //log/Logger.pop:()V
24: ireturn
25: iconst_0                 *2*
26: goto          35         *2*
29: astore_1
30: invokestatic  #31                 //log/Logger.pop:()V
33: aload_1
34: athrow
35: invokestatic  #31                 //log/Logger.pop:()V
38: ireturn

Exception table:
 from    to  target type
     5    15    15   any     *1*
    25    29    29   any     *2*
}

不可能将整个构造函数包装到 try-finally 块 因为 try 块不能跨越对超级构造函数的调用。虽然我在规范中找不到这个限制,但我可以找到两张讨论它的票:JDK-8172282, asm #317583.

如果您不关心构造函数,。这是一个简单的解决方案,在许多情况下可能都很好。然而,这个答案描述了一个不需要生成第二种方法的替代解决方案。


解决方案大致是根据"Compiling finally" in the JVM specification. The solution uses the JSR instruction. The instruction is not supported since langue level 7. Therefore, we use the JSRInlinerAdapter替换后面的说明

我们将从创建我们自己的 MethodVisitor 开始。请注意,我们扩展 MethodNode 而不是 MethodVisitor。我们这样做是为了在将信息传递给下一个访问者之前收集整个方法。稍后会详细介绍。

public class MyMethodVisitor extends MethodNode {

访客需要三个标签。第一个标签指定原始内容的开始和 try 块 的开始。第二个标签指定原始内容的结尾和 try 块 的结尾。它还指定异常处理程序的开始。最后一个标签指定代表 finally 块.

的子例程
  private final Label originalContentBegin = new Label();
  private final Label originalContentEnd = new Label();
  private final Label finallySubroutine = new Label();

构造函数重用了MethodVisitor的字段mvMethodNode 未使用它。我们也可以创建自己的领域。构造函数还创建 JSRInlinerAdapter 来替换上面提到的 JSR 指令。

  public MyMethodVisitor(
      MethodVisitor methodVisitor,
      int access, String name, String descriptor,
      String signature, String[] exceptions)
  {
    super(Opcodes.ASM8, access, name, descriptor, signature, exceptions);
    mv = new JSRInlinerAdapter(methodVisitor, access, name, descriptor, signature, exceptions);
  }

接下来,我们声明生成字节码的方法,这些字节码将在执行原始代码之前和之后执行。

  protected void generateBefore() { /* Generate your code here */ }
  protected void generateAfter() { /* Generate your code here */ }

根据the Javadoc of MethodVisitorASM调用

  • visitCode()方法内容被访问前,
  • visitMaxs(int,int)访问方法内容后

ASM访问该方法的内容之前,我们要注入我们自己的字节码并访问我们指定原始内容开始的标签。

  @Override
  public void visitCode() {
    super.visitCode();
    generateBefore();
    super.visitLabel(originalContentBegin);
  }

每当原始方法returns,我们要调用finally块的代码。

  @Override
  public void visitInsn(int opcode) {
    if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
      super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    }
    super.visitInsn(opcode);
  }

在方法的最后,我们为 try 块 和包含 finally 块 的子例程注入异常处理程序。

  @Override
  public void visitMaxs(int maxStack, int maxLocals) {
    super.visitLabel(originalContentEnd);
    super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    super.visitInsn(Opcodes.ATHROW);

    super.visitLabel(finallySubroutine);
    super.visitVarInsn(Opcodes.ASTORE, 0);
    generateAfter();
    super.visitVarInsn(Opcodes.RET, 0);

    super.visitMaxs(maxStack, maxLocals);
  }

最后,我们必须创建 try-catch 块 并将该方法转发给下一个方法访问者。由于 visitTryCatchBlock(…) 调用的不利顺序(请参阅 issue #317617),我们无法使用访问者模式更早地创建 try-catch 块。这就是为什么我们扩展 MethodNode 而不是 MethodVisitor.

  @Override
  public void visitEnd() {
    super.visitEnd();
    tryCatchBlocks.add(new TryCatchBlockNode(
        getLabelNode(originalContentBegin),
        getLabelNode(originalContentEnd),
        getLabelNode(originalContentEnd),
        null));
    accept(mv);
  }
}

由于转换不适用于构造函数,我们的方法 visitor 可以像这样在 ClassVisitor.

中使用
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
  if (name.equals("<init>")) {
    return super.visitMethod(access, name, descriptor, signature, exceptions);
  }
  else {
    return new MyMethodVisitor(
        super.visitMethod(access, name, descriptor, signature, exceptions),
        access, name, descriptor, signature, exceptions);
  }
}

还有一些改进的空间。

  • 您可以避免使用 JSR 指令并删除 JSRInlinerAdapter。这也可能提供一些减少生成代码大小的机会,因为 JSRInlinerAdapter 可能会多次复制 finally 块 的代码。

  • 即使您无法捕获超级构造函数的异常,您也可以在调用超级构造函数之前和之后添加对处理异常的构造函数的有限支持。

无论如何,此更改也可能使代码更加复杂。