您不应该直接调用访问者的方法。
使用ClassVisitor 的正确方法是使用您感兴趣的类的类文件字节创建ClassReader,并将类访问者传递给其accept 方法。然后,所有visit 方法将被类读取器根据在类文件中找到的工件调用。
在这方面,您不应仅仅因为它引用的是旧版本号就认为文档已过时。例如。 this document 正确描述了该过程,它代表库在版本 2 和 5 之间不需要进行根本性更改。
不过,访问课程并不会改变它。它有助于在遇到某个工件时对其进行分析并执行操作。请注意,返回null 不是实际操作。
如果你想创建一个修改过的类,你需要一个ClassWriter 来生成这个类。 ClassWriter 实现了ClassVisitor,也实现了class visitors can be chained,因此您可以轻松地创建一个委托给编写者的自定义访问者,这将生成一个与原始文件相同的类文件,除非您重写一个方法来拦截一个功能。
但请注意,从 visitMethod 返回 null 不仅仅删除代码,它还会完全删除方法。相反,您必须为特定方法返回一个特殊访问者,该访问者将重现该方法但忽略旧代码并创建唯一的 return 指令(您可以省略源代码中的最后一个 return 语句,但不能return 字节码中的指令)。
private static byte[] disableMethod(Method method) {
Class<?> theClass = method.getDeclaringClass();
ClassReader cr;
try { // use resource lookup to get the class bytes
cr = new ClassReader(
theClass.getResourceAsStream(theClass.getSimpleName()+".class"));
} catch(IOException ex) {
throw new IllegalStateException(ex);
}
// passing the ClassReader to the writer allows internal optimizations
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new MethodReplacer(
cw, method.getName(), Type.getMethodDescriptor(method)), 0);
byte[] newCode = cw.toByteArray();
return newCode;
}
static class MethodReplacer extends ClassVisitor {
private final String hotMethodName, hotMethodDesc;
MethodReplacer(ClassWriter cw, String name, String methodDescriptor) {
super(Opcodes.ASM5, cw);
hotMethodName = name;
hotMethodDesc = methodDescriptor;
}
// invoked for every method
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String signature, String[] exceptions) {
if(!name.equals(hotMethodName) || !desc.equals(hotMethodDesc))
// reproduce the methods we're not interested in, unchanged
return super.visitMethod(access, name, desc, signature, exceptions);
// alter the behavior for the specific method
return new ReplaceWithEmptyBody(
super.visitMethod(access, name, desc, signature, exceptions),
(Type.getArgumentsAndReturnSizes(desc)>>2)-1);
}
}
static class ReplaceWithEmptyBody extends MethodVisitor {
private final MethodVisitor targetWriter;
private final int newMaxLocals;
ReplaceWithEmptyBody(MethodVisitor writer, int newMaxL) {
// now, we're not passing the writer to the superclass for our radical changes
super(Opcodes.ASM5);
targetWriter = writer;
newMaxLocals = newMaxL;
}
// we're only override the minimum to create a code attribute with a sole RETURN
@Override
public void visitMaxs(int maxStack, int maxLocals) {
targetWriter.visitMaxs(0, newMaxLocals);
}
@Override
public void visitCode() {
targetWriter.visitCode();
targetWriter.visitInsn(Opcodes.RETURN);// our new code
}
@Override
public void visitEnd() {
targetWriter.visitEnd();
}
// the remaining methods just reproduce meta information,
// annotations & parameter names
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return targetWriter.visitAnnotation(desc, visible);
}
@Override
public void visitParameter(String name, int access) {
targetWriter.visitParameter(name, access);
}
}
自定义MethodVisitor 不会链接到类编写器返回的方法访问者。以这种方式配置,它不会自动复制代码。相反,默认情况下不执行任何操作,只有我们对 targetWriter 的显式调用才会生成代码。
在该过程结束时,您将拥有一个 byte[] 数组,其中包含类文件格式的更改代码。所以问题是,如何处理它。
您可以做的最简单、最便携的事情是创建一个新的ClassLoader,它从这些字节中创建一个新的Class,它具有相同的名称(因为我们没有更改名称),但是与已经加载的类不同,因为它有不同的定义类加载器。我们只能通过反射来访问这样的动态生成的类:
public class BytecodeMods {
public static void main(String[] args) throws Exception {
byte[] code = disableMethod(BytecodeMods.class.getMethod("test"));
new ClassLoader() {
Class<?> get() { return defineClass(null, code, 0, code.length); }
} .get()
.getMethod("test").invoke(null);
}
public static void test() {
System.out.println("This is a test");
}
…
为了让这个例子做的事情比什么都不做更引人注目,你可以改变消息,
使用以下MethodVisitor
static class ReplaceStringConstant extends MethodVisitor {
private final String matchString, replaceWith;
ReplaceStringConstant(MethodVisitor writer, String match, String replacement) {
// now passing the writer to the superclass, as most code stays unchanged
super(Opcodes.ASM5, writer);
matchString = match;
replaceWith = replacement;
}
@Override
public void visitLdcInsn(Object cst) {
super.visitLdcInsn(matchString.equals(cst)? replaceWith: cst);
}
}
通过改变
return new ReplaceWithEmptyBody(
super.visitMethod(access, name, desc, signature, exceptions),
(Type.getArgumentsAndReturnSizes(desc)>>2)-1);
到
return new ReplaceStringConstant(
super.visitMethod(access, name, desc, signature, exceptions),
"This is a test", "This is a replacement");
如果您想更改已加载类的代码或在加载到 JVM 之前拦截它,您必须使用 Instrumentation API。
字节码转换本身不会改变,您必须将源字节传递到 ClassReader 并从 ClassWriter 中取回修改后的字节。像ClassFileTransformer.transform(…) 这样的方法将已经接收到表示类当前形式的字节(可能有以前的转换)并返回新的字节。
问题是,这个 API 通常不适用于 Java 应用程序。它适用于所谓的 Java 代理,它们必须要么通过启动选项与 JVM 一起启动,要么以特定于实现的方式动态加载,例如通过 Attach API。
package documentation 描述了 Java 代理的一般结构和相关的命令行选项。
this answer 末尾的程序演示了如何使用 Attach API 附加到您自己的 JVM 以加载一个虚拟 Java 代理,该代理将使程序能够访问 Instrumentation API。考虑到复杂性,我认为很明显,实际的代码转换和将代码转换为运行时类或使用它来动态替换类,是两个必须协作的不同任务,但您通常需要其代码保持分开。