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

Java 在try-finally块中嵌入方法的现有代码(2),java,bytecode,instrumentation,java-bytecode-asm,bytecode-manipulation,Java,Bytecode,Instrumentation,Java Bytecode Asm,Bytecode Manipulation,不久前,我在《如何使用ASM在try-finally块中包装方法体》一书中提出了问题。 解决方案是访问visitCode()中方法体开头的try块标签,并在访问visitInsn()中带有返回操作码的指令时完成try finally块。我知道,如果一个方法没有返回指令,那么解决方案将无法工作。如果该方法总是带着异常离开,则返回指令将适用 不过,我发现前一种解决方案有时也不适用于带有返回指令的方法。如果一个方法有多条返回指令,它将不起作用。原因是它生成无效字节码,因为在方法的开头添加了一个try-

不久前,我在《如何使用ASM在try-finally块中包装方法体》一书中提出了问题。 解决方案是访问
visitCode()
中方法体开头的try块标签,并在访问
visitInsn()中带有返回操作码的指令时完成try finally块。我知道,如果一个方法没有返回指令,那么解决方案将无法工作。如果该方法总是带着异常离开,则返回指令将适用

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

通常(但可能取决于javac编译器),字节码方法包含一条返回指令,所有返回路径都通过跳到该指令结束。但是,使用Eclipse编译以下代码将生成带有两条返回指令的字节码:

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编译器所做的工作,尝试…最终…
,这意味着将
最终
操作复制到保留受保护(源)代码块的每个点(即返回指令)并安装多个受保护(生成字节码)区域(因为它们不应涵盖您的
finally
操作),但它们可能都指向同一个异常处理程序。或者,您可以转换代码,用分支将所有返回指令替换为“after”的一个实例操作之后是唯一的返回指令

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

例如,将
public void required()
更改为
private void required$instrumented()
并添加新的

public void desired() {
    //some logging X

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

请注意,由于调试信息保留在重命名的方法中,因此如果重命名的方法中引发异常,堆栈跟踪将继续报告正确的行号。如果仅通过添加不可见字符重命名它(请记住,在字节码级别您有更多的自由度),它会非常平滑。

多亏了霍尔格的回答和锑的评论,我开发了以下解决方案,满足了我的需求。 后来我发现类似的方法也在E.Kuleshov,AOSD.07,2007年3月,加拿大温哥华中进行了描述

此解决方案不适用于不包含非异常返回的方法(在每个执行路径中引发异常的方法,例如
引发新的NotSupportedOperationException();

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

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

我使用一个简单的
MethodVisitor
来访问代码。在
visitCode()
方法中,我添加了在输入方法时要执行的指令。然后,我通过访问一个新的
标签来添加try块的开头
我将完成try块并添加finally块。Moveover,我添加一个新的
标签
,以开始一个新的try块,以防该方法包含进一步的返回说明。(如果没有返回说明,则按照标签访问不会产生任何效果。)

简化代码如下:

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;
   }
}

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

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


该解决方案大致基于。该解决方案使用JSR指令。自Language level 7以来,该指令不受支持。因此,我们随后使用替换指令

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

public class MyMethodVisitor extends MethodNode {
访问者需要三个标签。第一个标签指定原始内容的开始和try块的开始。第二个标签指定原始内容的结束和try块的结束。它还指定异常处理程序的开始。最后一个标签指定表示finally块的子例程

  @Override
  public void visitInsn(int opcode) {
    if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
      super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    }
    super.visitInsn(opcode);
  }
构造函数重用
MethodVisitor
mv
字段。它不被
MethodNode
使用。我们也可以创建自己的字段。构造函数还创建了替换上述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 */ }
根据ASM的电话

  • visitCode()  @Override
      public void visitCode() {
        super.visitCode();
        generateBefore();
        super.visitLabel(originalContentBegin);
      }
    
      @Override
      public void visitInsn(int opcode) {
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
          super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
        }
        super.visitInsn(opcode);
      }
    
      @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);
      }
    
      @Override
      public void visitEnd() {
        super.visitEnd();
        tryCatchBlocks.add(new TryCatchBlockNode(
            getLabelNode(originalContentBegin),
            getLabelNode(originalContentEnd),
            getLabelNode(originalContentEnd),
            null));
        accept(mv);
      }
    }
    
    @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);
      }
    }