我想向特定类的每一行添加一个方法调用。为此,我想使用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);
}
我的问题是:为什么第一件事不起作用而是第二件事?
编辑:更多上下文:
我想在每行代码中插入方法visitVisit()。一个可能的示例类是这样的:
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语句之后,这会在验证类文件时导致错误。
有关该主题的任何新想法?
答案 0 :(得分:1)
这里有两个问题。
首先,似乎在调用Instrumentation.retransformClasses
时,JVM并没有报告转换后的代码(例如VerifyError
)的错误,而是直接处理旧代码。 / p>
我在这里看不到任何改变JVM行为的方法。值得创建一个额外的测试环境,在该环境中,您可以使用其他方法来激活代码,例如加载时转换或只是静态地转换已编译的类并尝试加载它们。一旦生产测试没有显示错误,生产代码可能会与retransformClasses
使用相同的转换代码。
在实现ClassFileTransformer
时,应将作为byte[]
方法的参数收到的transform
数组传递给ClassReader(byte[])
构造函数,而不要使用ClassReader(String)
构造函数。
第二,最后报告的行号的代码位置也是分支目标。请记住,换行符不会生成代码,因此循环的结束与return
语句的开始相同。
ASM将按以下顺序报告关联的工件:
visitLabel
和一个与代码位置相关联的Label
实例visitLineNumber
,其中包含新的行号和上一步中的Label
visitFrame
报告与此代码位置(因为它是分支目标)相关联的堆栈映射框架您要在visitLineNumber
调用处插入一条新指令,这会导致分支目标在此新指令之前 ,因为您在此之前委托了visitLabel
。但是visitFrame
调用在插入新指令后 被委托,因此不再与分支目标关联。这会导致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()
之类的东西。
请注意,采用这种设计,由于注入的指令总是放在另一条指令之前,因此不可能在方法的最后一条指令之后错误地注入指令。