javabytecodeinstrumentationjava-bytecode-asmbytecode-manipulation

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.


Solution

  • 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.