改写类并改写方法

改写类并改写方法

改写类,就是基于被改写的类的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