ASM字节码操作工具小试

参考《深入理解JVM字节码》、《ASM4 中文手册》

从书中 demo 到按自己的想法引出共写了 4 个 demo。

首先创建被操作的类,DemoClass,结构非常简单。

// DemoClass
public class DemoClass {
    public int a;
    public int b;
}

Demo1:ASM 访问 Field 和 Method

public class TestVisit {
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader(DemoClass.class.getName());
        /**
         * 0 手动计算
         * COMPUTE_MAXS 自动计算 帧、操作数栈大小和局部变量表大小
         * COMPUTE_FRAMES 类似2,不再调用 visitFrame
         */
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new ClassVisitor(ASM5, cw) {
            // 通过重写 visitField 和 visitMethod 可以做类似切面的操作 
            @Override
            public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
                System.out.println("field: " + name);
                return super.visitField(access, name, descriptor, signature, value);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                System.out.println("method: " + name);
                return super.visitMethod(access, name, descriptor, signature, exceptions);
            }
        };
        cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
    }
}

Demo2:ASM 添加 Field

/**
 * 不创建新的字节码文件,直接读取 bytes 创建 Class 对象
 */
public class TestVisit2 extends ClassLoader {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassReader cr = new ClassReader(DemoClass.class.getName());
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new ClassVisitor(ASM5, cw) {
            @Override
            public void visitEnd() {
                super.visitEnd();
                // 通过 visitField() 添加 Field
                FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC, "xyz",
                        "Ljava/lang/String;", null, null);
                if (fv != null) fv.visitEnd();
            }
        };
        cr.accept(cv, 0);
        // 转换成 bytes
        byte[] bytes = cw.toByteArray();
        // 通过 ClassLoader.defineClass() 从 bytes 生成 Class 对象
        Class<?> clazz = new TestVisit2().defineClass("DemoClass", bytes, 0, bytes.length);
        Field[] fields = clazz.getDeclaredFields();
        System.out.println(Arrays.toString(fields));
    }
}

Demo3:ASM 添加 Field (生成新的类)

对旧类进行修改后,创建新的字节码文件,这里要求新的 .class 文件名的同时也要改变类名(相当于变成一个新的类)。

/**
 * 创建一个新的增强类
 */
public class TestVisit3 extends ClassLoader {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ClassReader cr = new ClassReader(DemoClass.class.getName());
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new ClassVisitor(ASM5, cw) {
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                // 在 visit 的 name 部分进行修改,更改生成的类名
                super.visit(version, access, name+"Enhancer", signature, superName, interfaces);
            }

            @Override
            public void visitEnd() {
                super.visitEnd();
                FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC, "xyz",
                        "Ljava/lang/String;", null, null);
                if (fv != null) fv.visitEnd();
            }
        };

        cr.accept(cv, 0);
        byte[] bytes = cw.toByteArray();
        // 通过 FileUtils 存储字节码文件
        FileUtils.writeByteArrayToFile(new File(TestVisit3.class
                .getClassLoader()
                .getResource("")
                .getPath() + "DemoClassEnhancer.class"), bytes);
        // 可以拿到字节码文件,正常加载。相当于创建了一个新的类。
        Class<?> clazz = Class.forName("DemoClassEnhancer");
        Field[] fields = clazz.getDeclaredFields();
        System.out.println(Arrays.toString(fields));
    }
}

Demo4:ASM 添加 Filed 并修改 <init>

然后我尝试在Demo3的情况下,再给 DemoClass 初始化赋值,如下:

public class DemoClass {
    public int a = 0;
    public int b = 1;
}

会发现开始报错了,查看 DemoClassEnhancer:

public class DemoClassEnhancer {
    public int a;
    public int b;
    public String xyz;

    public DemoClassEnhancer() {
        super.a = 0;
        super.b = 1;
    }
}

很明显,这个 super.a、super.b 并不是想要的。一个简单的办法是,重写 DemoClassEnhancer 的 <init> 部分。

这里我复制了上面的 DemoClass,修改其名字为 DemoClass2。

/**
 * DemoClass2 中添加初始化值后,就会报错
 */
public class TestVisit4 extends ClassLoader {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ClassReader cr = new ClassReader(DemoClass2.class.getName());
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor cv = new ClassVisitor(ASM5, cw) {
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(version, access, name+"Enhancer", signature, superName, interfaces);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                // 去掉之前的 <init> 方法,因为下面将添加一个新的
                if ("<init>".equals(name)) {
                    return null;
                }
                return super.visitMethod(access, name, descriptor, signature, exceptions);
            }

            @Override
            public void visitEnd() {
                super.visitEnd();
                FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC, "xyz",
                        "Ljava/lang/String;", null, null);
                if (fv != null) fv.visitEnd();

                MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
                        "()V", null, null);
                mv.visitCode();
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                mv.visitInsn(ICONST_0);
                mv.visitFieldInsn(PUTFIELD, "DemoClass2Enhancer", "a", "I");
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                mv.visitInsn(ICONST_1);
                mv.visitFieldInsn(PUTFIELD, "DemoClass2Enhancer", "b", "I");
                mv.visitInsn(RETURN);
                // COMPUTE_MAXS 和 COMPUTE_FRAMES 并不是指令,所以需要手动触发计算,值随便填
                mv.visitMaxs(0,0);
                mv.visitEnd();
            }
        };

        cr.accept(cv, 0);
        byte[] bytes = cw.toByteArray();
        FileUtils.writeByteArrayToFile(new File(TestVisit4.class
                .getClassLoader()
                .getResource("")
                .getPath() + "DemoClass2Enhancer.class"), bytes);
        Class<?> clazz = Class.forName("DemoClass2Enhancer");
        Field[] fields = clazz.getDeclaredFields();
        System.out.println(Arrays.toString(fields));
    }
}