我正在尝试为an issue about missing stackmap frames的变通办法编写单元测试,但为此,我将需要生成一个类,如果缺少堆栈映射框架,该类将无法在Java 8上进行验证。
在下面,您可以查看我的测试用例(相关性:ASM,Guava,JUnit)。它从GuineaPig类中删除了堆栈映射框架,以希望其字节码无法通过验证。我遇到的问题是用最少的代码(需要堆栈映射框架)填充GuineaPig中的TODO,以便测试可以通过。
import com.google.common.io.*;
import org.junit.*;
import org.junit.rules.ExpectedException;
import org.objectweb.asm.*;
import java.io.*;
import static org.objectweb.asm.Opcodes.ASM5;
public class Java6MissingStackMapFrameFixerTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
public static class GuineaPig {
public GuineaPig() {
// TODO: make me require stackmap frames
}
}
@Test
public void example_class_cannot_be_loaded_because_of_missing_stackmap_frame() throws Exception {
byte[] originalBytecode = getBytecode(GuineaPig.class);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MethodVisitor(ASM5, super.visitMethod(access, name, desc, signature, exceptions)) {
@Override
public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
// remove the stackmap frames in order to cause a VerifyError
// super.visitFrame(type, nLocal, local, nStack, stack);
}
};
}
};
new ClassReader(originalBytecode).accept(cv, 0);
byte[] transformedBytecode = cw.toByteArray();
// Files.asByteSink(new File("test.class")).write(transformedBytecode);
thrown.expect(VerifyError.class);
thrown.expectMessage("Expecting a stackmap frame");
Class<?> clazz = new TestingClassLoader().defineClass(transformedBytecode);
clazz.newInstance();
}
private static byte[] getBytecode(Class<?> clazz) throws IOException {
String classFile = clazz.getName().replace(".", "/") + ".class";
try (InputStream b = clazz.getClassLoader().getResourceAsStream(classFile)) {
return ByteStreams.toByteArray(b);
}
}
private static class TestingClassLoader extends ClassLoader {
public Class<?> defineClass(byte[] bytecode) {
ClassReader cr = new ClassReader(bytecode);
String className = cr.getClassName().replace("/", ".");
return this.defineClass(className, bytecode, 0, bytecode.length);
}
}
}
最佳答案
理论
Java VM规范§4.10.1(通过类型检查进行验证)指定何时需要堆栈映射框架。首先,它给出了一个非正式的描述:
详细规范在§4.10.1.6(带代码的类型检查方法)中给出。 goto
命令需要堆栈映射框架:
以及所有其他分支命令:
同样,异常处理程序的开头也需要一个堆栈映射框架:
最后,§4.10.1.9(类型检查指令)指定哪些指令需要带有堆栈映射框架的分支目标。在类型规则中查找targetIsTypeSafe
;指令goto
,if*
,lookupswitch
和tableswitch
都有它。
例
即使以下代码也需要堆栈映射框架:
public static class GuineaPig {
public GuineaPig() {
int i = 1;
if (i > 0) {
// code branch to require stackmap frames
}
}
}
如果缺少它们,则代码将失败并出现异常:
java.lang.VerifyError: Expecting a stackmap frame at branch target 10
Exception Details:
Location:
net/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig.<init>()V @7: ifle
Reason:
Expected stackmap frame at this location.
Bytecode:
0000000: 2ab7 000c 043c 1b9e 0003 b1
at java.lang.Class.getDeclaredConstructors0(Native Method)
at java.lang.Class.privateGetDeclaredConstructors(Class.java:2658)
at java.lang.Class.getConstructor0(Class.java:2964)
at java.lang.Class.newInstance(Class.java:403)
这是字节码:
public net.orfjackal.retrolambda.Java6MissingStackMapFrameFixerTest$GuineaPig();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: iconst_1
5: istore_1
6: iload_1
7: ifle 10
10: return
LineNumberTable:
line 22: 0
line 23: 4
line 24: 6
line 27: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lnet/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig;
6 5 1 i I
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ class net/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig, int ]
stack = []
附言我花了一些时间弄清楚这一点,因为默认情况下,我以代码覆盖率运行我的单元测试,而IDEA的代码覆盖率工具显然会自动重新计算所有类的堆栈映射框架,这消除了我的测试为删除它们所做的努力。