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.
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.
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.
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.
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.
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.
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:
visitFrame
if you instantiate the ClassWriter
with the COMPUTE_FRAMES
flag.AdviceAdapter
and to perform the bytecode manipulation in its onMethodEnter()
and onMethodExit()
methods.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*
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With