方法访问者在 Java ASM visitLineNumber() 中不起作用
Method visitor is not working in Java ASM visitLineNumber()
我想为特定 class 的每一行添加一个方法调用。为此,我想使用 ASM(基于访问者)库。
不工作的部分意味着没有插入代码(方法调用)。
我在 MethodVisitor class 中的(不工作的)代码到目前为止看起来像这样:
@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()。一个可能的例子 class 是这个:
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);
}
这会打印出访问过的所有行 classes 但是我想添加的方法调用没有添加或者至少没有被执行。
编辑:发现了一些新东西:
如果 visitLineNumber 插入没有在方法的最后一行插入内容,它就会起作用。
例如上面的计算器class:
只要第 7 行(return 行)中没有插入代码,代码就可以正常工作。我尝试了另一个 class 和 2 return 语句,它也工作正常,直到它到达最后一个 return 语句。
我认为插入方法调用的顺序有误。可能它是在 return 语句之后插入的,这会在验证 class 文件时导致错误。
关于这个话题有什么新想法吗?
这里有两个问题。
首先,似乎在调用 Instrumentation.retransformClasses
时,JVM 不会报告转换代码的错误,如 VerifyError
,而是简单地继续处理旧代码.
我看不出有什么方法可以改变 JVM 的行为。值得创建一个额外的测试环境,您可以在其中使用不同的方法来激活代码,例如加载时转换或只是静态转换已编译的 classes 并尝试加载它们。这可能是对使用与 retransformClasses
相同的转换代码的生产代码的补充,一旦这些测试没有显示错误。
顺便说一句,当您实现 ClassFileTransformer
时,您应该将收到的 byte[]
数组作为 transform
方法的参数传递给 ClassReader(byte[])
构造函数使用 ClassReader(String)
构造函数。
其次,最后报告行号的代码位置也是一个分支目标。请记住,换行符不会生成代码,因此循环的结尾与 return
语句的开头相同。
ASM 将按以下顺序报告关联的工件:
visitLabel
具有与代码位置关联的 Label
实例
visitLineNumber
加上新的行号和上一步的 Label
visitFrame
报告与此代码位置关联的堆栈映射框架(因为它是分支目标)
您正在 visitLineNumber
调用中插入一条新指令,这导致分支目标在 这条新指令之前 ,因为您委托了 visitLabel
之前。但是 visitFrame
调用在 插入新指令后被委托 ,因此不再与分支目标相关联。这会导致 VerifyError
,因为每个分支目标都必须有一个堆栈映射框架。
一个简单但昂贵的解决方案是,不使用原始的 class' 堆栈映射帧,而是让 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()
或类似的东西。
请注意,使用此设计,也不可能在方法的最后一条指令之后错误地注入指令,因为注入的指令总是位于另一条指令之前。
我想为特定 class 的每一行添加一个方法调用。为此,我想使用 ASM(基于访问者)库。
不工作的部分意味着没有插入代码(方法调用)。
我在 MethodVisitor class 中的(不工作的)代码到目前为止看起来像这样:
@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()。一个可能的例子 class 是这个:
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);
}
这会打印出访问过的所有行 classes 但是我想添加的方法调用没有添加或者至少没有被执行。
编辑:发现了一些新东西:
如果 visitLineNumber 插入没有在方法的最后一行插入内容,它就会起作用。
例如上面的计算器class: 只要第 7 行(return 行)中没有插入代码,代码就可以正常工作。我尝试了另一个 class 和 2 return 语句,它也工作正常,直到它到达最后一个 return 语句。
我认为插入方法调用的顺序有误。可能它是在 return 语句之后插入的,这会在验证 class 文件时导致错误。
关于这个话题有什么新想法吗?
这里有两个问题。
首先,似乎在调用 Instrumentation.retransformClasses
时,JVM 不会报告转换代码的错误,如 VerifyError
,而是简单地继续处理旧代码.
我看不出有什么方法可以改变 JVM 的行为。值得创建一个额外的测试环境,您可以在其中使用不同的方法来激活代码,例如加载时转换或只是静态转换已编译的 classes 并尝试加载它们。这可能是对使用与 retransformClasses
相同的转换代码的生产代码的补充,一旦这些测试没有显示错误。
顺便说一句,当您实现 ClassFileTransformer
时,您应该将收到的 byte[]
数组作为 transform
方法的参数传递给 ClassReader(byte[])
构造函数使用 ClassReader(String)
构造函数。
其次,最后报告行号的代码位置也是一个分支目标。请记住,换行符不会生成代码,因此循环的结尾与 return
语句的开头相同。
ASM 将按以下顺序报告关联的工件:
visitLabel
具有与代码位置关联的Label
实例visitLineNumber
加上新的行号和上一步的Label
visitFrame
报告与此代码位置关联的堆栈映射框架(因为它是分支目标)
您正在 visitLineNumber
调用中插入一条新指令,这导致分支目标在 这条新指令之前 ,因为您委托了 visitLabel
之前。但是 visitFrame
调用在 插入新指令后被委托 ,因此不再与分支目标相关联。这会导致 VerifyError
,因为每个分支目标都必须有一个堆栈映射框架。
一个简单但昂贵的解决方案是,不使用原始的 class' 堆栈映射帧,而是让 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()
或类似的东西。
请注意,使用此设计,也不可能在方法的最后一条指令之后错误地注入指令,因为注入的指令总是位于另一条指令之前。