Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Some time ago, I asked in Embed the existing code of a method in a try-finally block how to wrap the body of a method in a try-finally block using ASM. The solution was to visit a label for the try block at the beginning of the method body in visitCode() and to complete the try-finally block when visiting an instruction with a return opcode in visitInsn(). I was aware that the solution won't be working if a method has no return instruction which applies if the method is always leaving with an exception.

Though, I discovered that the former solution is sometimes inappropriate for methods with return instructions, too. It won't be working if a method has more than one return instruction. The reason is that it generates invalid bytecode because one try-finally block is added at the beginning of the method but more than one try-finally block is completed.

Usually (but probably depending on the javac compiler), a bytecode method contains a single return instruction and all return paths end at that instruction by jumping there. However, the compilation of the following code with Eclipse will lead to byte code with two return instructions:

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

Byte code compiled with 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

Thus, I am wondering what the proper way to wrap the whole code of a method code is.

like image 311
nrainer Avatar asked Feb 09 '15 17:02

nrainer


People also ask

What is the finally block in a method?

What Is finally? finally defines a block of code we use along with the try keyword. It defines code that's always run after the try and any catch block, before the method is completed. The finally block executes regardless of whether an exception is thrown or caught.

What is the use of finally () method in Java?

The finally keyword is used to create a block of code that follows a try block. A finally block of code always executes, whether or not an exception has occurred. Using a finally block allows you to run any cleanup-type statements that you just wish to execute, despite what happens within the protected code.

How do you handle exceptions in finally block?

The "finally" block execution stops at the point where the exception is thrown. Irrespective of whether there is an exception or not "finally" block is guaranteed to execute. Then the original exception that occurred in the try block is lost.

What is a try finally block?

By using a finally block, you can clean up any resources that are allocated in a try block, and you can run code even if an exception occurs in the try block. Typically, the statements of a finally block run when control leaves a try statement.


Video Answer


2 Answers

You have to retrace what a Java compiler does when compiling try … finally … which implies copying your finally action to every point where the protected (source) code block will be left (i.e. return instruction) and install multiple protected (resulting byte code) regions (as they shouldn’t cover your finally action) but they may all point to the same exception handler. Alternatively you can transform the code, replacing all return instruction by a branch to one instance of your “after” action followed by a sole return instruction.

That’s not trivial. So if you don’t need Hot Code Replace which usually doesn’t support adding methods to a loaded class, the easiest way to avoid all this is to rename the original method to a name not clashing with others (you may use characters not allowed in ordinary source code) and create a new method using the old name and signature which consists of a simple try … finally … construct containing an invocation of the renamed method.

E.g. change public void desired() to private void desired$instrumented() and add a new

public void desired() {
    //some logging X

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

Note that since the debug information remains at the renamed method, stack traces will continue to report the correct line numbers if an exception is thrown in the renamed method. If you rename it by just adding an invisible character (keep in mind that you have more freedom at byte code level), it will like quite smooth.

like image 84
Holger Avatar answered Oct 28 '22 07:10

Holger


Thanks to the answer of Holger and the comments of Antimony I developed the following solution which satisfies my needs. Later I found that a similar approach is also described in Using ASM framework to implement common bytecode transformation patterns, E. Kuleshov, AOSD.07, March 2007, Vancouver, Canada.

This solution does not work for methods which contain no non-exceptional return (methods which throw an exception in every execution path, such as throw new NotSupportedOperationException();)!

If you need to support these methods as well you should follow Holger's advice to rename the original method and then to add a new method with the old name. Add a delegate call in the added method to the renamed method and embed the invocation in a try-finally block.


I use a simple MethodVisitor to visit the code. In the visitCode() method I add instructions to be executed when entering the method. Then, I add the beginning of the try block by visiting a new Label. When I visit a return opcode in visitInsn() I will complete the try block and add the finally block. Moveover, I add a new Label to begin a new try block in case the method contains further return instructions. (If no return instructions follow the label visit won't have any effect.)

The simplified code is as follows:

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

Notes:

  • You are not supposed to invoke visitFrame if you instantiate the ClassWriter with the COMPUTE_FRAMES flag.
  • It is also possible (and possibly preferable) to use the AdviceAdapter and to perform the bytecode manipulation in its onMethodEnter() and onMethodExit() methods.
  • As mentioned earlier, the try-finally block will only be added if the bytecode contains at least one return instruction.

The transformed byte code output for the isEven() method of the question will be:

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*
}
like image 44
6 revs Avatar answered Oct 28 '22 05:10

6 revs