使用ASM动态生成class文件

  在java语言中,Java文件在编译时会将java文件编译成.class的字节码文件。通常我们在写代码时只涉及到java文件开发,但有时候在一些特殊的场景下,比如在代码中统一插入一些性能监控的代码,我们可以利用像字节码插桩这样的动态修改class文件的技术来实现。动态修改生成class文件也是AOP编程的基础。我们还是有必要去了解一下的。下面介绍一下如何利用ASM来实现动态生成class文件。
首先在项目用引入ASM的test依赖包,注意这个依赖包只能在test目录下使用。

testImplementation 'org.ow2.asm:asm:7.1'
testImplementation 'org.ow2.asm:asm-commons:7.1'

接着我们来写一段要实现出来的代码:

public class InjectTest {

    public InjectTest() {
    }

    @ASMTest
    public static void main(String[] args) throws InterruptedException {
        // 通过ASM动态添加
        long var1 = System.currentTimeMillis();
        Thread.sleep(3000);
        // 通过ASM动态添加
        long var3 = System.currentTimeMillis();
        // 通过ASM动态添加
        System.out.println("时间是:" + (var3 - var1));
    }

    void method() {}

}

上面这段代码中,假如我们的项目中有一段耗时的代码,这里用Thread.sleep(3000)模拟,我们需要去计算这段代码执行到底耗费了多长时间,然后将其打印出来,那么利用ASM应该怎么做呢。

@Test
    public void test() {
        try {
            // 获取已经生成的class文件
            FileInputStream fis = new FileInputStream("/Users/iosdev/Desktop/Demo/ByteCodeInstDemo2/app/src/test/java/com/example/bytecodeinstdemo2/InjectTest.class");

            // 获取一个分析器, 去读class文件
            ClassReader classReader = new ClassReader(fis);
            // 获取一个分析器, 去写class文件
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

            // 开始插桩, 用最新版本ASM7,
            classReader.accept(new MyClassVisitor(Opcodes.ASM7, classWriter), ClassReader.EXPAND_FRAMES);

            // 将写完的内容转换成byte数组
            byte[] bytes = classWriter.toByteArray();
            // 将修改后的class文件写入(路径是绝对路径)
            FileOutputStream fos = new FileOutputStream("/Users/iosdev/Desktop/Demo/ByteCodeInstDemo2/app/src/test/java/com/example/bytecodeinstdemo2/InjectTest2.class");
            // 将bytes写到文件中
            fos.write(bytes);

            fos.close();
            fis.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 用来访问类信息的
     */
    static class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int api) {
            super(api);
        }

        public MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        // 读取class文件信息的时候, 每读到一个方法, 就执行下面的代码一次
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
        }
    }

    /**
     * 这里用来访问方法的
     */
    static class MyMethodVisitor extends AdviceAdapter {

        protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
        }

        // 局部空间表的下标
        int s;

        /**
         * 方法进入的时候执行
         */
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();

            if (inJect == false) {
                return;
            }

            invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
            // 保存数据类型为long类型
            s = newLocal(Type.LONG_TYPE);
            // 存放在布局变量表下标为1的位置
            storeLocal(1);

        }

        // 局部空间表的下标
        int e;

        /**
         * 方法退出的时候执行
         * @param opcode
         */
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);

            if (inJect == false) {
                return;
            }

            invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
            e = newLocal(Type.LONG_TYPE);
            storeLocal(e);

            getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));

//            L3
//            LINENUMBER 13 L3
//            GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
            //分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
            newInstance(Type.getType("Ljava/lang/StringBuilder;"));
//            NEW java/lang/StringBuilder
            dup();
//            DUP
//            visitLdcInsn("");
//            LDC "\u65f6\u95f4\u662f:"
            invokeConstructor(Type.getType("Ljava/lang/StringBuilder.<init>"), new Method("Ljava/lang/String;", "V"));
//            INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
            loadLocal(3);
//            LLOAD 3
//            LLOAD 1
//            LSUB
//            INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
//            INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
//            INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
//                    L4
//            LINENUMBER 14 L4
//                    RETURN
            visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);

        }

        boolean inJect = false;

        /**
         * 如果一个方法上有注解, 就执行该方法
         * @param descriptor
         * @param visible
         * @return
         */
        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println(getName() + "->" + descriptor);
            // 这里表示, 如果注解信息中带有ASMTest,则inJect为true, 上面的方法中我们控制了只有inJect为true时才插入class字节码
            if ("Lcom/example/bytecodeinstdemo2/ASMTest;".equals(descriptor)) {
                inJect = true;
            }
            return super.visitAnnotation(descriptor, visible);
        }
    }

这里完成的一个功能是在原有的代码中, 插入一段计时的代码, 用来计算代码执行的时间。当然我觉得对于程序员来说,要自己去写一些字节码的调用方法是非常繁琐且耗费时间的。所以有没有什么办法能够更简洁更友好一点来帮助我们生成我们想要的class文件呢。当然是有的~首先我们得下载ASM插件,


ASM Bytecode Viewer插件.png

下载安装完之后,在使用时右键单击要展示byteCode的代码,会出现下面的选项:
image.png
,选中图片中的这个就可以了。注意我在使用时出现过一种情况就是test文件夹中的代码是没办法应用这个插件的,main文件夹下的项目文件是可以使用。还不知道什么原因。这面这个图片中的内容就是ASM Bytecode Viewer中打开的视图了。右边的部分可以看到,有一个”Bytecode“窗口视图,这个展示的是比较原生的字节码指令,对于我们来说很不好理解。当然我们也可以参照这个来对应写出操作字节码的方法。
ASM Bytecode视图.png

第二个选项是”ASMified“窗口视图。这个窗口展示的内容相对来说友好一些,虽然最后实现的代码是一样的,但是这里所展示的几乎跟我们平时写的java代码没什么区别了,所以如果我们要实现上面所实现的代码可以直接将这部分代码copy到项目中。
ASMified视图.png
public class HelloWorld2 implements Opcodes {

    public static final String PATH = "app/src/test/java/com/example/bytecodeinstdemo2/";

    public static void main(String[] args) {

        // 获取一个分析器, 去写class文件
        ClassWriter classWriter = new ClassWriter(0);
        // 变量的访问者,用来访问class文件中的变量
        FieldVisitor fieldVisitor;
        // 方法的访问者,用来访问class文件中的方法
        MethodVisitor methodVisitor;
        // 注解的访问者,用来访问class文件中的注解信息
        AnnotationVisitor annotationVisitor0;
        // 定义一个叫做Example的类
        // V1_1:生成的class的版本号
        // ACC_PUBLIC:表示该类的访问标识, 是一个public类
        // Example:生成的类的类名,这里是全限定名,如果有包名,则需要传入 包名+类名
        // signature:泛型相关的
        // superName:当前类的父类的全限定名
        // interfaces:传入当前要生成的类的直接实现的接口
        classWriter.visit(V1_7, ACC_PUBLIC | ACC_SUPER, "com/example/bytecodeinstdemo2/ASMDemo", null, "java/lang/Object", null);

        classWriter.visitSource("HelloWorld2.java", null);

        {
            // 定义一个方法, 这里表明是生成一个构造方法
            // ACC_PUBLIC: 方法的访问权限是public
            // <init>: 方法的方法名, 构造方法方法名是<init>
            // ()V: 方法描述符, 构造方法无参数,无返回值,描述符为()V
            // signature: 泛型相关
            // exceptions: 方法申明可能抛出的异常
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            // 读取源文件中代码的行数
            methodVisitor.visitLineNumber(5, label0);
            // 生成构造方法的字节码指令, 将第0个本地变量(也就是this)压入操作数栈。
            methodVisitor.visitVarInsn(ALOAD, 0);
            // 调用visitMethodInsn方法, 生成invokespecial指令, 调用父类(也就是Object)的构造方法。
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(6, label1);
            // 调用visitInsn方法,生成return指令, 方法返回。
            methodVisitor.visitInsn(RETURN);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLocalVariable("this", "Lcom/example/bytecodeinstdemo2/ASMDemo;", null, label0, label2, 0);
            // 调用visitMaxs方法, 指定当前要生成的方法的最大局部变量和最大操作数栈。 对应Code属性中的max_stack和max_locals 。
            methodVisitor.visitMaxs(1, 1);
            // 调用visitEnd方法, 表示当前要生成的构造方法已经创建完成。
            methodVisitor.visitEnd();
        }
        {
            // 生成main方法
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, new String[]{"java/lang/InterruptedException"});
            {
                annotationVisitor0 = methodVisitor.visitAnnotation("Lcom/example/bytecodeinstdemo2/ASMDemo;", false);
                annotationVisitor0.visitEnd();
            }
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(10, label0);
            // long var1 = System.currentTimeMillis();
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 1);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(11, label1);
            methodVisitor.visitLdcInsn(new Long(3000L));
            // Thread.sleep(3000);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(12, label2);
            // long var3 = System.currentTimeMillis();
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 3);
            Label label3 = new Label();
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLineNumber(13, label3);
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
            methodVisitor.visitInsn(DUP);
            // System.out.println("时间是:" + (var3 - var1));
            methodVisitor.visitLdcInsn("\u65f6\u95f4\u662f:");
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V", false);
            methodVisitor.visitVarInsn(LLOAD, 3);
            methodVisitor.visitVarInsn(LLOAD, 1);
            methodVisitor.visitInsn(LSUB);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            Label label4 = new Label();
            methodVisitor.visitLabel(label4);
            methodVisitor.visitLineNumber(14, label4);
            methodVisitor.visitInsn(RETURN);
            Label label5 = new Label();
            methodVisitor.visitLabel(label5);
            methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label5, 0);
            methodVisitor.visitLocalVariable("var1", "J", null, label1, label5, 1);
            methodVisitor.visitLocalVariable("var3", "J", null, label3, label5, 3);
            methodVisitor.visitMaxs(6, 5);
            methodVisitor.visitEnd();
        }
        {
            // 生成method方法
            methodVisitor = classWriter.visitMethod(0, "method", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(16, label0);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lcom/example/bytecodeinstdemo2/ASMDemo;", null, label0, label1, 0);
            methodVisitor.visitMaxs(0, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        // 获取生成的class文件对应的二进制流
        byte[] code = classWriter.toByteArray();

        //将二进制流写到本地磁盘上
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(PATH + "ASMDemo.class");
            fos.write(code);
            fos.close();
            //直接将二进制流加载到内存中
            Helloworld loader = new Helloworld();
//            Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);

            //通过反射调用main方法
//            exampleClass.getMethods()[0].invoke(null, new Object[] { null });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面的代码等同于最开始我们贴出来的源码。

参考文章:
https://blog.csdn.net/zhangjg_blog/article/details/22976929
https://www.jianshu.com/p/16ed4d233fd1
https://www.jianshu.com/p/13d18c631549

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容