正如这个答案中所解释的,ASM 计算(最具体的)公共超类的方法不一定会重现原始类的堆栈图帧。它不仅需要访问类(您可以解决),还可以访问原始代码从未引用过的类,因为原始代码使用了更抽象的类型或接口类型,或者因为原始框架实际上删除了随后未使用的值,而不是声明合并类型。
因此,更可取的方法是根据您所做的代码修改,根据原始帧计算堆栈图帧。对于您的预期用例,这很容易,因为您没有更改代码的分支结构,而只是注入代码,使堆栈状态与插入的代码片段之前完全相同。
所以原则上,应该可以只使用原始帧。要实现这一点,不要指定COMPUTE_FRAMES
到ClassWriter
并且不要指定SKIP_FRAMES
到ClassReader
. 如果原始大小小于 2,您只需调整最大堆栈大小,以确保您的方法参数有空间。
您的代理的实际问题来自尝试使用源代码行来确定插入调用的代码位置。为了说明这一点,请考虑以下示例:
public class Example {
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
我使用以下代码来显示哪些 ASM 调用将发送给您的访问者:
public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("Example");
cr.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(name+desc);
return new PrintingVisitor();
}
}, 0);
}
static class PrintingVisitor extends MethodVisitor {
final Map<Label,Integer> labels = new HashMap<>();
public PrintingVisitor() {
super(Opcodes.ASM5);
}
private String name(Label label) {
return "label_"+labels.merge(label, labels.size(), (a,b) -> a);
}
@Override public void visitCode() {
System.out.println("visitCode()");
}
@Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
System.out.println("visitFrame()");
}
@Override public void visitLabel(Label label) {
System.out.println("."+name(label));
}
@Override public void visitLineNumber(int line, Label start) {
System.out.println(".line "+line+", "+name(start));
}
@Override public void visitJumpInsn(int opcode, Label label) {
System.out.println(get(opcode)+" "+name(label));
}
@Override public void visitInsn(int opcode) {
System.out.println(get(opcode));
}
@Override
public void visitIincInsn(int var, int increment) {
System.out.println("iinc "+var+", "+increment);
}
@Override public void visitEnd() {
System.out.println();
}
}
static String get(int opcode) {
// for simplification, just the ones we need
switch(opcode) {
case Opcodes.RETURN: return "return";
case Opcodes.ICONST_0: return "iconst_0";
case Opcodes.ILOAD: return "iload";
case Opcodes.IF_ICMPGE: return "if_icmpge";
case Opcodes.GOTO: return "goto";
default: return "<"+opcode+">";
}
}
产生(用 编译时javac
):
main([Ljava/lang/String;)V
visitCode()
.label_0
.line 3, label_0
iconst_0
.label_1
visitFrame()
if_icmpge label_2
.label_3
.line 4, label_3
.label_4
.line 3, label_4
iinc 1, 1
goto label_1
.label_2
.line 6, label_2
visitFrame()
return
.label_5
这表明:
- “第一行”,即第 3 行被报告两次,因为循环在其末尾生成与
for
循环语句的位置相关联的代码
- “最后一行”,即第6行在
visitFrame()
描述循环结束的分支目标的堆栈状态之前报告。label_2
用于报告源代码行和作为if_icmpge
指令的目标。将visitLabel
调用委托给 时,您正在定义分支目标,并且分支目标需要堆栈图框架,因此and调用ClassWriter
之间必须没有代码,但是您用来插入代码的调用是在它们之间进行的.visitLabel
visitFrame
visitLineNumber
解决方案:
visitCode()
在方法开始的调用处注入代码。那是在其他任何事情发生之前,并且不会与任何后续操作发生冲突:
@Override public void visitCode() {
super.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "raiseDepth", "()V", false);
}
对于在方法末尾注入代码,只需使用可以结束方法的精确指令,即
@Override public void visitInsn(int opcode) {
switch(opcode) {
case RETURN: case ARETURN: case IRETURN: case LRETURN: case FRETURN: case DRETURN:
case ATHROW:
mv.visitMethodInsn(INVOKESTATIC, "de/ugoe/cs/listener/CallHelper", "lowerDepth", "()V", false);
}
super.visitInsn(opcode);
}
请注意,这不足以获得finally
在每种情况下都调用该方法的类似语义。例如,当调用的方法抛出异常或运行时生成异常时,就像取消引用null
或除以零时一样,该方法可能不会被调用,但您的原始代码存在问题。
对于在任意源代码行注入代码,没有直接的解决方案。如图所示,源代码行不会 1:1 映射到字节码位置,并且报告的位置可能位于无法注入的位置。最好选择一个附加标准,如易于识别的代码结构,例如,一个已知的方法调用,插入到它之前或之后。