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

时间:2015-02-09 17:40:35

标签: java bytecode instrumentation java-bytecode-asm bytecode-manipulation

前一段时间,我在Embed the existing code of a method in a try-finally block中询问如何使用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

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

3 个答案:

答案 0 :(得分:3)

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

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

E.g。将public void desired()更改为private void desired$instrumented()并添加新的

public void desired() {
    //some logging X

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

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

答案 1 :(得分:1)

感谢Holger的回答和锑的评论我开发了以下满足我需求的解决方案。 后来我发现在Using ASM framework to implement common bytecode transformation patterns,E。Kuleshov,AOSD.07,2007年3月,加拿大温哥华也有类似的方法。

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

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


我使用简单的MethodVisitor来访问代码。在visitCode()方法中,我添加了在输入方法时要执行的指令。然后,我通过访问新的Label添加try块的开头。当我在visitInsn()中访问返回操作码时,我将完成try块并添加finally块。转移,我添加一个新的Label来开始一个新的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;
   }
}

备注:

  • 如果您使用visitFrame标记实例化ClassWriter,则不应调用COMPUTE_FRAMES
  • 也可以(并且可能更好)使用AdviceAdapter并在其onMethodEnter()onMethodExit()方法中执行字节码操作。
  • 如前所述,只有在字节码包含至少一条返回指令时才会添加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*
}

答案 2 :(得分:1)

不可能将整个构造函数包装到 try-finally块 中,因为 try块无法覆盖对超级构造函数的调用。尽管我在规范中找不到此限制,但可以找到两个讨论它的票证:JDK-8172282asm #317583

如果您不关心构造函数,请you can wrap methods into other methods that catch the exceptions as written by Holger。这是一个简单的解决方案,在许多情况下可能会很好。但是,此答案描述了一种不需要生成第二种方法的替代解决方案。


该解决方案大致基于"Compiling finally" in the JVM specification。该解决方案使用 JSR 指令。从语言7级开始不支持该指令。因此,我们在以后使用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();

构造函数重用mv的字段MethodVisitorMethodNode不使用它。我们也可以创建自己的领域。构造函数还创建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 MethodVisitor ASM 呼叫

  • visitCode()在访问方法的内容之前,并且
  • visitMaxs(int,int)在访问方法的内容之后。

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

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

无论原始方法何时返回,我们都希望调用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);
  }
}

由于该转换不适用于构造函数,因此可以在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块的代码。

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

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