java - 方法访问者在 Java ASM VisitLineNumber() 中不起作用

标签 java java-bytecode-asm

我想向特定类的每一行添加方法调用。为此,我想使用 ASM(基于访问者)库。

不工作部分表示代码(方法调用)未插入。

我在 MethodVisitor 类中的(不工作的)代码到目前为止看起来像这样:

@Override
public void visitLineNumber(int line, Label start) {
  mv.visitMethodInsn(
      Opcodes.INVOKESTATIC,
      classpath,
      "visitLine",
      "()V",
      false);
  super.visitLineNumber(line, start);

我尝试了 MethodVisitor 的另一种方法,它工作得很好,如下所示:

@Override
public void visitInsn(int opcode) {
  mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          classpath,
          "visitLine",
          "()V",
          false);
  super.visitInsn(opcode);
}

我的问题是:为什么第一件事不起作用,而第二件事却不起作用?

编辑:更多上下文:

我想在每一行代码中插入方法调用visitLine()。一个可能的示例类是这样的:

public class Calculator {
  public int evaluate(final String pExpression) {
    int sum = 0;
    for (String summand : pExpression.split("\\+")) {
      sum += Integer.parseInt(summand);
    }
    return sum;
  }
}

变成:

public class Calculator {
  public int evaluate(final String pExpression) {
    OutputWriter.visitLine();
    int sum = 0;
    OutputWriter.visitLine();
    for (String summand : pExpression.split("\\+")) {
      OutputWriter.visitLine();
      sum += Integer.parseInt(summand);
    }
    OutputWriter.visitLine();
    return sum;
  }
}

我对 ClassReader、ClassWriter 和 ClassVisitor 进行了基本设置,如下所示:

ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(pClassName);
ClassVisitor tcv = new TransformClassVisitor(cw);
cr.accept(tcv, 0);
return cw.toByteArray();

在 MethodVisitor 中我只重写这个方法:

@Override
  public void visitLineNumber(int line, Label start) {
    System.out.println(line);
    mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            classpath,
            "visitLine",
            "()V",
            false);
    super.visitLineNumber(line, start);
  }

这会打印出访问的类的所有行,但我想要添加的方法调用未添加或至少未执行。

编辑:发现了一些新东西:

如果没有在方法的最后一行插入某些内容,visitLineNumber 插入就会起作用。

例如上面的计算器类: 只要第 7 行(返回行)中没有插入代码,代码就可以正常工作。我尝试了另一个带有 2 个 return 语句的类,它也工作得很好,直到到达最后一个 return 语句。

我认为插入方法调用的顺序有错误。也许它被插入到 return 语句之后,这会导致验证类文件时出错。

关于这个主题有什么新想法吗?

最佳答案

这里有两个问题。

首先,似乎当调用 Instrumentation.retransformClasses 时,JVM 不会报告转换代码的错误,例如 VerifyError,而是会报告只需继续使用旧代码即可。

我在这里看不到任何改变 JVM 行为的方法。值得创建一个额外的测试环境,在其中使用不同的方法来激活代码,例如加载时转换或仅静态转换编译的类并尝试加载它们。一旦这些测试没有显示错误,这可能是对生产代码的补充,生产代码使用与 retransformClasses 相同的转换代码。

顺便说一句,当您实现 ClassFileTransformer 时,您应该将收到的 byte[] 数组作为 transform 方法的参数传递到 ClassReader(byte[]) 构造函数,而不是使用 ClassReader(String) 构造函数。

<小时/>

其次,最后报告的行号的代码位置也是分支目标。请记住,换行符不会生成代码,因此循环的结束与 return 语句的开始相同。

ASM 将按以下顺序报告关联的工件:

  • visitLabel 以及与代码位置关联的 Label 实例
  • visitLineNumber 以及上一步中的新行号和 Label
  • visitFrame 报告与此代码位置关联的堆栈映射框架(因为它是分支目标)

您将在 visitLineNumber 调用中插入一条新指令,这会导致分支目标位于该新指令之前,因为您委托(delegate)了 visitLabel 之前。但是,在插入新指令之后,visitFrame 调用会被委托(delegate),因此不再与分支目标关联。这会导致 VerifyError,因为每个分支目标都必须有一个堆栈图框架。

一个简单但昂贵的解决方案是,不使用原始类的堆栈映射框架,而是让 ASM 重新计算它们。即

public static byte[] getTransformed(byte[] originalCode) {
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    ClassReader cr = new ClassReader(originalCode);
    ClassVisitor tcv = new TransformClassVisitor(cw);
    cr.accept(tcv, ClassReader.SKIP_FRAMES);
    return cw.toByteArray();
}

顺便说一句,当你保留大部分原始代码但只插入一些新语句时,通过 passing the ClassReader to the ClassWriter’s constructor 优化流程是有好处的:

public static byte[] getTransformed(byte[] originalCode) {
    ClassReader cr = new ClassReader(originalCode);
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
    ClassVisitor tcv = new TransformClassVisitor(cw);
    cr.accept(tcv, ClassReader.SKIP_FRAMES);
    return cw.toByteArray();
}

使用 ASM 的 API 来说,不重新计算堆栈图帧(因为原始帧仍然适合这种简单的转换)的更有效的解决方案并不那么容易。到目前为止,我唯一的想法是推迟新指令的插入,直到访问该帧(如果有的话)。不幸的是,这意味着重写所有 visit 方法以获取说明:

留下来

public static byte[] getTransformed(byte[] originalCode) {
    ClassReader cr = new ClassReader(originalCode);
    ClassWriter cw = new ClassWriter(cr, 0);
    ClassVisitor tcv = new TransformClassVisitor(cw);
    cr.accept(tcv, 0);
    return cw.toByteArray();
}

并使用

static class Transformator extends MethodVisitor {
    int lastLineNumber;

    public Transformator(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }
    public void visitLineNumber(int line, Label start) {
        lastLineNumber = line;
        super.visitLineNumber(line, start);
    }
    private void checkLineNumber() {
        if(lastLineNumber > 0) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, classpath,"visitLine","()V", false);
            lastLineNumber = 0;
        }
    }
    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
        checkLineNumber();
        super.visitTryCatchBlock(start, end, handler, type);
    }
    public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
        checkLineNumber();
        super.visitMultiANewArrayInsn(descriptor, numDimensions);
    }
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        checkLineNumber();
        super.visitLookupSwitchInsn(dflt, keys, labels);
    }
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        checkLineNumber();
        super.visitTableSwitchInsn(min, max, dflt, labels);
    }
    public void visitIincInsn(int var, int increment) {
        checkLineNumber();
        super.visitIincInsn(var, increment);
    }
    public void visitLdcInsn(Object value) {
        checkLineNumber();
        super.visitLdcInsn(value);
    }
    public void visitJumpInsn(int opcode, Label label) {
        checkLineNumber();
        super.visitJumpInsn(opcode, label);
    }
    public void visitInvokeDynamicInsn(
        String name, String desc, Handle bsmHandle, Object... bsmArg) {
        checkLineNumber();
        super.visitInvokeDynamicInsn(name, desc, bsmHandle, bsmArg);
    }
    public void visitMethodInsn(
        int opcode, String owner, String name, String desc, boolean iface) {
        checkLineNumber();
        super.visitMethodInsn(opcode, owner, name, desc, iface);
    }
    public void visitFieldInsn(int opcode, String owner, String name,String descriptor) {
        checkLineNumber();
        super.visitFieldInsn(opcode, owner, name, descriptor);
    }
    public void visitTypeInsn(int opcode, String type) {
        checkLineNumber();
        super.visitTypeInsn(opcode, type);
    }
    public void visitVarInsn(int opcode, int var) {
        checkLineNumber();
        super.visitVarInsn(opcode, var);
    }
    public void visitIntInsn(int opcode, int operand) {
        checkLineNumber();
        super.visitIntInsn(opcode, operand);
    }
    public void visitInsn(int opcode) {
        checkLineNumber();
        super.visitInsn(opcode);
    }
}

不幸的是,ASM 的访问者模型没有 preVisitInstr() 或类似的东西。

请注意,通过这种设计,也不可能在方法的最后一条指令之后错误地注入(inject)指令,因为注入(inject)的指令始终放置在另一条指令之前。

关于java - 方法访问者在 Java ASM VisitLineNumber() 中不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53359308/

相关文章:

java - 在ASM中能否获取putfield或putstatic指令的Field实例?

java - ASM之前看一下maxStack指令吗?

java - 为什么我的数据库仅在前两个 jsp 表单提交时更新成功?

java - 在Java中逐字符复制文件

java - Jmeter BeanShell 断言解析并比较 json 中的 UTC 日期时间

javassist : cannot parse method body with parameterized Maps/Lists

java - iload_1、iload_2、iload_3 和 iload #index 字节码有什么区别?

java - 为什么 jersey-bundle 1.17.1 中的 asm 提供了作用域?

java - JPA:一个关于entityManager.joinTransaction的问题

java - `Caused by: java.lang.RuntimeException: view must have a tag` 的实际含义是什么?