我正在尝试删除以下程序中test()
的方法体,以便不会向控制台打印任何内容。我正在使用ASM 5.2,但我尝试过的所有东西似乎都没有任何效果。
有人可以解释我做错了什么,并指出了一些关于ASM的最新教程或文档吗?我在Stackoverflow和ASM网站上发现的几乎所有内容都显得过时和/或无用。
public class BytecodeMods {
public static void main(String[] args) throws Exception {
disableMethod(BytecodeMods.class.getMethod("test"));
test();
}
public static void test() {
System.out.println("This is a test");
}
private static void disableMethod(Method method) {
new MethodReplacer()
.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, method.getName(), Type.getMethodDescriptor(method), null, null);
}
public static class MethodReplacer extends ClassVisitor {
public MethodReplacer() {
super(Opcodes.ASM5);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return null;
}
}
}
答案 0 :(得分:8)
您不应该直接调用访问者的方法。
使用ClassVisitor
的正确方法是使用您感兴趣的类的类文件字节创建ClassReader
,并将类访问者传递给其accept
方法。然后,类阅读器将根据类文件中的工件调用所有visit
方法。
在这方面,您不应该考虑过时的文档,只是因为它引用了较旧的版本号。例如。 this document正确描述了该过程,并且它为图书馆说明版本2和版本5之间不需要进行根本性的更改。
尽管如此,参观课程并没有改变它。它有助于分析它并在遇到某个工件时执行操作。请注意,返回null
不是实际操作。
如果要创建修改后的类,则需要ClassWriter
来生成类。 ClassWriter
实现了ClassVisitor
,还有class visitors can be chained,因此您可以轻松创建委托给编写者的自定义访问者,这将生成与原始文件相同的类文件,除非您覆盖方法拦截特征的再创造。
但请注意,从null
返回visitMethod
不仅仅是删除代码,它还会完全删除该方法。相反,您必须返回特定方法的特殊访问者,该方法将重现该方法但忽略旧代码并创建唯一的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
,它具有相同的名称(因为我们没有更改name),但与已加载的类不同,因为它有一个不同的定义类加载器。我们只能通过Reflection:
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的权限。考虑到复杂性,我认为,显而易见的是,实际的代码转换以及将代码转换为运行时类或使用它来动态替换类,是两个必须协作的不同任务,但是您通常需要的代码保持分离。
答案 1 :(得分:0)
更简单的方法是创建一个MethodNode实例并用新的InsnList替换主体。首先,您需要原始的类表示。你可以像@Holger建议的那样得到它。
Class<?> originalClass = method.getDeclaringClass();
ClassReader classReader;
try {
cr = new ClassReader(
originalClass.getResourceAsStream(originalClass.getSimpleName()+".class"));
} catch(IOException e) {
throw new IllegalStateException(e);
}
然后创建一个ClassNode并替换方法体。
//Create the CLassNode
ClassNode classNode = new ClassNode();
classReader.accept(classNode,0);
//Search for the wanted method
final List<MethodNode> methods = classNode.methods;
for(MethodNode methodNode: methods){
if(methodNode.name.equals("test")){
//Replace the body with a RETURN opcode
InsnList insnList = new InsnList();
insnList.add(new InsnNode(Opcodes.RETURN));
methodNode.instructions = insnList;
}
}
在生成新类之前,您需要一个带有公共defineClass()方法的ClassLoader。就像这样。
public class GenericClassLoader extends ClassLoader {
public Class<?> defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
现在你可以生成实际的类了。
//Generate the Class
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
classNode.accept(classWriter);
//Define the representation
GenericClassLoader classLoader = new GenericClassLoader();
Class<?> modifiedClass = classLoader.defineClass(classNode.name, classWriter.toByteArray());