改写类并改写方法
改写类并改写方法
改写类,就是基于被改写的类的class字节数组修改生成新的类,一般改写类不会修改类的名称,因为修改类的名称或使用不同类加载器加载,那对虚拟机而言就是一个新的类。Java Agent允许我们在类加载之前修改类,也可结合Attach API使用,在类加载之后,程序运行期间可随时修改类,通过重新加载类替换旧的类。关于Java Agent和Attach API见本书第七章。
改写一个类首先需要获取到一个类的字节数组。ASM框架提供的ClassReader用于解析符合class文件格式的字节数组,我们可通过使用ClassReader读取并解析获取到一个类的字节数组,再将解析后的字节数组交给ClassWriter去改写。
ClassReader的构造方法有五个,如下。
public ClassReader(final String className) throws IOException public ClassReader(final byte[] classFile) public ClassReader(final InputStream inputStream) throws IOException public ClassReader( final byte[] classFileBuffer, final int classFileOffset, final int classFileLength) ClassReader( final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion)
前四个构造方法是我们可以使用的,且最后都会调用到最后一个构造方法。第一个构造方法可直接传递类名,ASM根据类名从当前classpath去读取该类的class文件;第二个构造方法可直接传递一个符合class文件结构的字节数组;第三个构造方法是传递一个输入流,ASM从输入流读取字节数据。
以使用ClassReader的传递一个类名的构造方法创建一个ClassReader为例,代码如下。
String className = "com.wujiuye.asmbytecode.book.fifth.UseAsmModifyClass"; ClassReader classReader = new ClassReader(className);
ClassReader并不是一个访问者,但ClassReader提供了accept方法用于传递一个ClassVisitor,由该ClassVisitor访问ClassReader解析后的class字节数组。accept方法的定义如下。
public void accept(final ClassVisitor classVisitor, final int parsingOptions)
accept方法各参数解析:
◆classVisitor:类访问者,如ClassWriter;
◆parsingOptions:解析选项,可以是SKIP_CODE、SKIP_DEBUG、SKIP_FRAMES、EXPAND_FRAMES中的零个或多个。零个传0,多个使用“或”运算符组合。
我们来看一个ClassReader与ClassWriter结合使用的例子:从classpath中读取一个现有类,并给该类添加一个字段,然后将改写后的类的字节数组输出到文件。如代码清单5-13所示。
代码清单5-13 改写类为类添加一个字段
public class UseAsmModifyClass { public static void main(String[] args) throws IOException { String className = "com.wujiuye.asmbytecode.book.fifth.UseAsmModifyClass"; ClassReader classReader = new ClassReader(className); ClassWriter classWriter = new ClassWriter(0); classReader.accept(classWriter, 0); generateField(classWriter); byte[] newByteCode = classWriter.toByteArray(); ByteCodeUtils.savaToFile(className, newByteCode); } static void generateField(ClassWriter classWriter) { FieldVisitor fieldVisitor = classWriter.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null); fieldVisitor.visitAnnotation("Llombok/Getter;", false); } }
在UseAsmModifyClass类的main方法中,我们先创建一个ClassReader实例,用来读取当前类UseAsmModifyClass的class文件并解析,然后创建一个ClassWriter,调用ClassReader实例的accept方法传入该ClassWriter,之后调用generateField方法为该类生成一个私有的String类型的字段,字段名为name,并为该字段添加一个@Getter注解。最后调用ByteCodeUtils工具类的savaToFile方法将改写后的class字节数组输出到文件。改写后的类如图5.4所示。

图5.4 添加字段后的UseAsmModifyClass
为该类添加方法与“从头开始”创建一个新的类并为类添加方法一样,可通过调用ClassWriter实例的visitMethod方法为类添加方法。但改写方法就不能简单的通过调用ClassWriter实例的visitMethod方法完成了。
以移除UseAsmModifyClass类的main方法为例。我们需要自己实现一个ClassVisitor,但我们不做ClassWriter能做的事情,只做ClassWriter不能做的事情,因此我们可以使用代理模式实现自定义的ClassVisitor。自定义的ClassVisitor如代码清单5-14所示。
代码清单5-14 MyClassWriter类
public class MyClassWriter extends ClassVisitor { private ClassWriter classWriter; public MyClassWriter(ClassWriter classWriter) { super(Opcodes.ASM6, classWriter); this.classWriter = classWriter; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if ("main".equals(name)) { return null; } return super.visitMethod(access, name, descriptor, signature, exceptions); } public byte[] toByteArray() { return classWriter.toByteArray(); } }
自定义的MyClassWriter重写了ClassVisitor的visitMethod方法,当方法名称为“main”时,返回null,否则调用父类的visitMethod方法,由父类调用MyClassWriter构造参数传入的ClassWriter的visitMethod方法创建main方法与创建main方法访问者。
能够移除main方法的原理是,当visitMethod方法返回null时,main方法不会创建,也不会创建main方法的访问者,因此不会创建“main”方法。我们在调用ClassReader实例的accept方法时,accept方法会遍历ClassReader实例读取到的类的class文件结构的各项,遍历的目的是调用ClassWriter实例的相应visit方法,将ClassReader实例读取到的类的文件结构各项填充到ClassWriter实例的class文件结构。
现在我们可以使用自定义的MyClassWriter改写UseAsmModifyClass类,去掉UseAsmModifyClass的main方法,如代码清单5-15所示。
代码清单5-15 使用MyClassWriter改写UseAsmModifyClass
public class UseAsmModifyClass { public static void main(String[] args) throws IOException { String className = "com.wujiuye.asmbytecode.book.fifth.UseAsmModifyClass"; ClassReader classReader = new ClassReader(className); // 创建MyClassWriter实例 MyClassWriter classWriter = new MyClassWriter(new ClassWriter(0)); classReader.accept(classWriter, 0); byte[] newByteCode = classWriter.toByteArray(); ByteCodeUtils.savaToFile(className, newByteCode); } }
改写后的UseAsmModifyClass类如图5.5所示。

图5.5 去掉main方法的UseAsmModifyClass
如果我们不想去掉main方法,只是想改变main方法中的代码,怎么实现呢?与自定义ClassVisitor一样,我们也可以通过自定义MethodVisitor实现。如在UseAsmModifyClass的main方法插入一行输出“hello word!”的代码。自定义的MainMethodWriter类如代码清单5-16所示。
代码清单5-16 MainMethodWriter
public class MainMethodWriter extends MethodVisitor { private MethodVisitor methodVisitor; public MainMethodWriter(MethodVisitor methodVisitor) { super(Opcodes.ASM6, methodVisitor); this.methodVisitor = methodVisitor; } @Override public void visitCode() { super.visitCode(); methodVisitor.visitFieldInsn(GETSTATIC, Type.getInternalName(System.class), "out", Type.getDescriptor(System.out.getClass())); methodVisitor.visitLdcInsn("hello word!"); methodVisitor.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(System.out.getClass()), "println", "(Ljava/lang/String;)V", false); } }
MainMethodWriter重写了visitCode方法,目的是在main方法的第一条字节码指令的前面插入输出“hello word!”的字节码指令。当然,也可以重写visitInsn方法,在main方法的return指令之前插入字节码指令,代码如下。
@Override public void visitInsn(int opcode) { if (opcode == RETURN) { // 如果操作码等于return指令的操作码 // 则在return指令之前插入一些字节码指令 } super.visitInsn(opcode); }
现在我们改写代码清单5-14中自定义的MyClassWriter,修改visitMethod方法,判断如果是main方法,则创建一个MainMethodWriter,如代码清单5-15所示。
代码清单5-15 修改MyClassWriter
public class MyClassWriter extends ClassVisitor { private ClassWriter classWriter; public MyClassWriter(ClassWriter classWriter) { super(Opcodes.ASM6, classWriter); this.classWriter = classWriter; } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if ("main".equals(name)) { MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); return new MainMethodWriter(methodVisitor); } return super.visitMethod(access, name, descriptor, signature, exceptions); } public byte[] toByteArray() { return classWriter.toByteArray(); } }
运行代码清单5-15的main方法,得到改写后的UseAsmModifyClass类如图5.6所示。

图5.6 改写后的UseAsmModifyClass