What I did is to instrument java classes at runtime to warp the whole method with a big try-catch block, and then rethrow the exception in the catch block if any exception is caught.
I use a Premain class to add a class file transformer to dynamically instrument the loaded java class. The Transformer class:
public class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws
IllegalClassFormatException {
byte[] result = classfileBuffer;
if (className == null){
return result;
}
...
try{
ClassReader cr = new ClassReader(result);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new CV(cw, className, loader, getClassVersion(cr));
cr.accept(cv, ClassReader.EXPAND_FRAMES);
result = cw.toByteArray();
} catch (Throwable t){
t.printStackTrace();
}
return result;
}
}
The ClassVisitor and MethodVisitor:
public class CV extends ClassVisitor {
private String slashClassName;
private ClassLoader loader;
private int classVersion;
public CV(ClassVisitor classVisitor, String className, ClassLoader loader, int classVersion) {
super(ASM_Version, classVisitor);
this.slashClassName = className;
this.loader = loader;
this.classVersion = classVersion;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MV(mv, slashClassName, name, descriptor, this.classVersion);
}
}
class MV extends MethodVisitor {
...
// insert a try catch block for the whole test method to capture the exception thrown
private Label tryStart = new Label();
private Label tryEndCatchStart = new Label();
public MV(MethodVisitor methodVisitor, String className, String methodName, String desc, int classVersion) {
super(ASM_Version, methodVisitor);
...
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitLabel(tryStart);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitTryCatchBlock(tryStart, tryEndCatchStart, tryEndCatchStart, "java/lang/Throwable");
mv.visitLabel(tryEndCatchStart);
mv.visitFrame(F_FULL, 0, null, 1, new Object[]{"java/lang/Throwable"});
mv.visitInsn(ATHROW);
mv.visitMaxs(maxStack+4, maxLocals);
}
}
But I got an error like this:
...
Caused by: java.lang.VerifyError: StackMapTable error: bad offset
Exception Details:
Location:
org/example/Test.personTest()V @0: new
Reason:
Invalid stackmap specification.
Current Frame:
bci: @147
flags: { }
locals: { }
stack: { 'java/lang/Throwable' }
Bytecode:
0x0000000: bb00 0259 bb00 0359 1204 1205 b700 06b7
0x0000010: 0007 b300 08b2 0008 bb00 0359 1209 120a
0x0000020: b700 06b5 000b b200 0cb2 000d b600 0eb2
0x0000030: 0008 b400 0b12 0fb5 0010 2ab7 0011 9900
0x0000040: 0bb2 000c 1212 b600 13b1 bf
Exception Handler Table:
bci [0, 74] => handler: 74
Stackmap Table:
same_frame_extended(@73)
full_frame(@147,{},{Object[#84]})
The original class before instrumented looks like:
public void personTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=6, locals=1, args_size=1
0: new #2 // class org/example/Person
3: dup
4: new #3 // class org/example/Name
7: dup
8: ldc #4 // String xxx
10: ldc #5 // String yyy
12: invokespecial #6 // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
15: invokespecial #7 // Method org/example/Person."<init>":(Lorg/example/Name;)V
18: putstatic #8 // Field president:Lorg/example/Person;
21: getstatic #8 // Field president:Lorg/example/Person;
24: new #3 // class org/example/Name
27: dup
28: ldc #9 // String a
30: ldc #10 // String b
32: invokespecial #6 // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
35: putfield #11 // Field org/example/Person.name:Lorg/example/Name;
38: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
41: getstatic #13 // Field name:Lorg/example/Name;
44: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
47: getstatic #8 // Field president:Lorg/example/Person;
50: getfield #11 // Field org/example/Person.name:Lorg/example/Name;
53: ldc #15 // String c
55: putfield #16 // Field org/example/Name.familyName:Ljava/lang/String;
58: aload_0
59: invokespecial #17 // Method ifxxx:()Z
62: ifeq 73
65: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
68: ldc #18 // String yes
70: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
73: return
LineNumberTable:
line 33: 0
line 35: 21
line 36: 38
line 37: 47
line 38: 58
line 39: 65
line 41: 73
LocalVariableTable:
Start Length Slot Name Signature
0 74 0 this Lorg/example/Test;
StackMapTable: number_of_entries = 1
frame_type = 251 /* same_frame_extended */
offset_delta = 73
RuntimeVisibleAnnotations:
0: #40()
The class after instrumentation looks like:
public void personTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=10, locals=1, args_size=1
0: new #2 // class org/example/Person
3: dup
4: new #3 // class org/example/Name
7: dup
8: ldc #4 // String xxx
10: ldc #5 // String yyy
12: invokespecial #6 // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
15: invokespecial #7 // Method org/example/Person."<init>":(Lorg/example/Name;)V
18: putstatic #8 // Field president:Lorg/example/Person;
21: getstatic #8 // Field president:Lorg/example/Person;
24: new #3 // class org/example/Name
27: dup
28: ldc #9 // String a
30: ldc #10 // String b
32: invokespecial #6 // Method org/example/Name."<init>":(Ljava/lang/String;Ljava/lang/String;)V
35: putfield #11 // Field org/example/Person.name:Lorg/example/Name;
38: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
41: getstatic #13 // Field name:Lorg/example/Name;
44: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
47: getstatic #8 // Field president:Lorg/example/Person;
50: getfield #11 // Field org/example/Person.name:Lorg/example/Name;
53: ldc #15 // String c
55: putfield #16 // Field org/example/Name.familyName:Ljava/lang/String;
58: aload_0
59: invokespecial #17 // Method ifxxx:()Z
62: ifeq 73
65: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
68: ldc #18 // String yes
70: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
73: return
74: athrow
Exception table:
from to target type
0 74 74 Class java/lang/Throwable
StackMapTable: number_of_entries = 2
frame_type = 251 /* same_frame_extended */
offset_delta = 73
frame_type = 255 /* full_frame */
offset_delta = 73
locals = []
stack = [ class java/lang/Throwable ]
LineNumberTable:
line 33: 0
line 35: 21
line 36: 38
line 37: 47
line 38: 58
line 39: 65
line 41: 73
LocalVariableTable:
Start Length Slot Name Signature
0 74 0 this Lorg/example/Test;
RuntimeVisibleAnnotations:
0: #40()
I found that the two frames in the StackMapTable have the same offset 73! That is absolutely wrong because
The bytecode offset at which a frame applies is calculated by adding offset_delta + 1 to the bytecode offset of the previous frame, unless the previous frame is the initial frame of the method, ...
So that is why it complains. However, I still don't know why the inserted frame has the same offset as the existing one (shouldn't it be 0?), is that just a coincidence?
You are mixing two incompatible options, EXPAND_FRAMES
and F_FULL
. From the documentation of visitFrame
:
The frames of a method must be given either in expanded form, or in compressed form (all frames must use the same format, i.e. you must not mix expanded and compressed frames within a single method):
- In expanded form, all frames must have the
F_NEW
type.- In compressed form, frames are basically "deltas" from the state of the previous frame:
Opcodes.F_SAME
representing frame with exactly the same locals as the previous frame and with the empty stack.Opcodes.F_SAME1
representing frame with exactly the same locals as the previous frame and with single value on the stack (numStack
is 1 andstack[0]
contains value for the type of the stack item).Opcodes.F_APPEND
representing frame with current locals are the same as the locals in the previous frame, except that additional locals are defined (numLocal
is 1, 2 or 3 and local elements contains values representing added types).Opcodes.F_CHOP
representing frame with current locals are the same as the locals in the previous frame, except that the last 1-3 locals are absent and with the empty stack (numLocal
is 1, 2 or 3).Opcodes.F_FULL
representing complete frame data.
Since you’re keeping all original frames and adding just one frame, it’s more efficient to remove the EXPAND_FRAMES
option, to keep all frames compressed.