Warning: file_get_contents(/data/phpspider/zhask/data//catemap/9/java/382.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
方法访问者在Java ASM visitLineNumber()中不工作_Java_Java Bytecode Asm - Fatal编程技术网

方法访问者在Java ASM visitLineNumber()中不工作

方法访问者在Java ASM visitLineNumber()中不工作,java,java-bytecode-asm,Java,Java Bytecode Asm,我想为特定类的每一行添加一个方法调用。为此,我想使用ASM(基于访问者)库 非工作部分表示代码(方法调用)未插入 到目前为止,MethodVisitor类中我的(不工作)代码如下所示: @Override public void visitLineNumber(int line, Label start) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, classpath, "visitLine", "(

我想为特定类的每一行添加一个方法调用。为此,我想使用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);
}
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(pClassName);
ClassVisitor tcv = new TransformClassVisitor(cw);
cr.accept(tcv, 0);
return cw.toByteArray();
我的问题是:为什么第一件事不起作用,而第二件事起作用

编辑:更多上下文:

我想在每一行代码中插入方法调用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的基本设置,如下所示:

@Override
public void visitInsn(int opcode) {
  mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          classpath,
          "visitLine",
          "()V",
          false);
  super.visitInsn(opcode);
}
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插入将起作用

例如,上面的calculator类: 只要第7行(返回行)中没有插入代码,代码就可以正常工作。我尝试了另一个包含2条返回语句的类,在到达最后一条返回语句之前,该类也运行良好

我认为插入方法调用的顺序有错误。可能是在return语句之后插入的,这会导致在验证类文件时出错


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

这里有两个问题

首先,当调用
Instrumentation.retransformClasses
时,JVM似乎不会报告转换代码中的错误,比如
VerifyError
,而只会继续处理旧代码

在这里,我看不到任何改变JVM行为的方法。创建一个额外的测试环境是值得的,在这里您可以使用不同的方法来激活代码,比如加载时转换,或者只是静态地转换编译后的类并尝试加载它们。这可能是对生产代码的补充,一旦这些测试显示没有错误,生产代码将使用相同的转换代码和
重传格式类

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


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

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

  • visitLabel
    ,带有与代码位置关联的
    标签
    实例
  • 使用上一步中的新行号和
    标签访问行号
  • visitFrame
    报告与此代码位置关联的堆栈映射框(因为它是分支目标)
您正在
visitLineNumber
调用中插入一条新指令,这会导致分支目标位于此新指令之前,因为您将
visitLabel
委托给它。但是
visitFrame
调用在插入新指令后被委派,因此不再与分支目标关联。这会导致一个
VerifyError
,因为每个分支目标都必须有一个stackmap帧

一个简单但昂贵的解决方案是,不使用原始类的stackmap框架,而是让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();
}
顺便说一句,当您保留大部分原始代码但只插入一些新语句时,通过以下方式优化流程是有益的:

不重新计算stackmap帧(因为原始帧仍然适用于这种简单的转换)的更有效的解决方案对于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()
之类的东西


注意,使用这种设计,也不可能在方法的最后一条指令之后错误地注入指令,因为注入的指令总是放在另一条指令之前。

您应该更具体地说明“不工作”部分。如果你的意思是,它从来没有被调用过,回想一下,它只能在特定的可选调试信息可用时被调用。是的,对不起,编辑了文本。我想用visitMethodInsn()插入的代码未插入。我以前试过的每一行都会调用它。当你说调用
visitLineNumber
时,没有可识别的错误。你必须提供更多的上下文。一般来说,源代码行和字节码指令之间的映射并不简单,在每行插入语句的想法很容易出错。是否有更好的方法在每一行中插入语句?添加了更多上下文。我希望你现在能理解我的问题。非常感谢你,这就是我一直在寻找的解决办法。