字节码操作框架介绍与实践(以ASM和Javassit为例)

简介: ASM是java字节码操作领域公认的标准,被众多知名的开源框架使用,如cglib、mybatis,fastjson等。通过ASM提供的API,我们可以方便的修改类文件的字节码,并ASM会自动帮我们做很多事情,如维护常量池的索引、计算栈大小max_stack,局部变量表大小max_locals等。ASM提供了两种类型的API,基于事件触发的core api和基于对象的tree api,下面主要介绍基

ASM是java字节码操作领域公认的标准,被众多知名的开源框架使用,如cglib、mybatis,fastjson等。通过ASM提供的API,我们可以方便的修改类文件的字节码,并ASM会自动帮我们做很多事情,如维护常量池的索引、计算栈大小max_stack,局部变量表大小max_locals等。ASM提供了两种类型的API,基于事件触发的core api和基于对象的tree api,下面主要介绍基于事件触发的core api。

1. ASM core api介绍

core api是基于事件驱动的,其中最核心的三个类是ClassReader,ClassVisitor与ClassWriter。

ClassReader主要负责class文件字节码的读取与分析,在解析类文件的各个节点与阶段会触发相应的事件(如ClassVisitor,MethodVisitor),我们可以通过重写事件的回调方法来改写字节码。

ClassVisitor是一个抽象类,当需要读取或改写类文件的字节码时,我们需要继承该类,ClassReader的accept方法需要传入一个ClassVisitor对象,ClassReader在解析class文件的过程中遇到不同的节点时会调用ClassVisitor的不同的visitXXX方法,如下:

各事件回调函数的调用顺序,其中visit最先被调用,接着调用0次或1次visitSource;调用0次或1次visitOuterClass;接下来任意顺序调用任意多次visitAnnotation和visitAttribute(取决于类文件的结构);然后任意顺序调用任意多次visitInnerClass,visitField,visitMethod;最后调用visitEnd。上述visitXXX的过程中还可能触发一些子过程,如visitAnnotation会触发AnnotationVisitor,visitMethod会触发MethodVisitor,在这些visitXXX的过程中(包括ClassVisitor,AnnotationVisitor,MethodVisitor等),我们可以去修改各个子节点的字节码。完整的调用顺序图如下:

ClassWriter是ClassVisitor的一个实现类,ClassWriter的toByteArray方法可以返回修改后的字节码的byte数组。ClassReader,ClassVisitor,ClassWriter的关系如下:

2. ASM操作字节码示例

首先创建一个MyMain类,如下:

目标是修改test01方法,如下:

    @SneakyThrows
    @Test
    public void testModifiedMethod() {
    	byte[] bytes = FileUtils.readFileToByteArray(new File(
            "/Users/xycode/IdeaProjects/techlecture/techlecture/src/main/java/com/xycode/techlecture/asm/MyMain"
                + ".class"));
        ClassReader classReader = new ClassReader(bytes);

        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);

        ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                String[] exceptions) {
                //先删除test01()方法
                if ("test01".equals(name)) {
                    return null;
                }
                return super.visitMethod(access, name, desc, signature, exceptions);
            }
        };
        classReader.accept(classVisitor, 0);

        byte[] removedMethodArray = classWriter.toByteArray();

        classReader = new ClassReader(removedMethodArray);

        //指定 自动计算操作数栈与局部变量表的大小
        classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {
            @Override
            public void visitEnd() {
                super.visitEnd();
                //新增test01()方法
                MethodVisitor methodVisitor = this.visitMethod(Opcodes.ACC_PUBLIC, "test01", "(I)I", null, null);
                if (methodVisitor != null) {
                    methodVisitor.visitCode();
                    methodVisitor.visitVarInsn(Opcodes.ILOAD, 1); // iload_1
                    methodVisitor.visitVarInsn(Opcodes.BIPUSH, 100); // bipush 100
                    methodVisitor.visitInsn(Opcodes.IADD); // iadd
                    methodVisitor.visitInsn(Opcodes.IRETURN); // ireturn
                    //notice: 设置了COMPUTE_MAXS后, 这里需要手动触发自动计算, 填的参数值无所谓
                    methodVisitor.visitMaxs(-1, -1);
                    methodVisitor.visitEnd();
                }
            }
        };

        classReader.accept(classVisitor, 0);

        byte[] modifiedMethodArray = classWriter.toByteArray();

        FileUtils.writeByteArrayToFile(new File(
            "/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/asm/MyMain5"
                + ".class"), modifiedMethodArray);
    }

生成的MyMain5.class文件反编译如下:

使用javap命令查看MyMain5.class的字节码指令,如下:

可以看出,红框中,即test01方法的字节码和我们在代码中添加的指令是一致的。

3. javassist介绍与实践

如上所示,ASM框架的使用门槛是比较高的,需要熟悉底层的JVM字节码指令。而javassist是一个性能稍逊ASM,但使用门槛很低的字节码操作框架,因此在业界也得到了很多应用。javassist的核心api如下所示:

其中CtClass用来代表一个类对象,通过CtClass可以修改、新增类的方法(CtMethod)与字段(CtField),ClassPool是javassist定义的一个容器,用于存放CtClass。下面来演示如何使用javassist:

首先创建一个MyMain类,如下:

目标是新增一个foo方法,首先看一下CtMethod的构造器用法,如下:

下面使用CtMethod来为MyMain类添加foo方法:

    ClassPool classPool = ClassPool.getDefault();
    //添加类扫描路径
    classPool.insertClassPath(new ClassClassPath(MyMain.class));

    //加载MyMain类
    CtClass ctClass = classPool.get(MyMain.class.getName());
    CtMethod ctMethod = new CtMethod(CtClass.doubleType, "foo", new CtClass[] {CtClass.intType, CtClass.doubleType},
        ctClass);

    //$1代表取局部变量表index为1的值, 以此类推
    ctMethod.setBody("return $1 * $2;");
    ctClass.addMethod(ctMethod);
    ctClass.writeFile("/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist");

生成的MyMain类文件反编译如下:

可以看出使用javassit来操作字节门槛低了很多,下面调用该类文件中的foo方法,如下:

    /**
     * 自定义类加载器
     */
    static class MyClassLoader extends ClassLoader {

        public Class<?> findMyClass(String name, byte[] bytes) throws IOException {
            return defineClass(name, bytes, 0, bytes.length);
        }
    }


    @Test
    public void testLoadClass() throws Exception{
    	MyClassLoader myClassLoader = new MyClassLoader();

        byte[] bytes = FileUtils.readFileToByteArray(new File("/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist/com/xycode/techlecture/asm/MyMain.class"));

        Class<?> clazz = myClassLoader.findMyClass("com.xycode.techlecture.asm.MyMain", bytes);
        Object o = clazz.newInstance();

        Method foo = clazz.getMethod("foo", int.class, double.class);
        System.out.println(foo.invoke(o, 123, 456.1));

    }

执行结果:

javassit也可以方便地对类文件中已经存在的方法进行修改,如下:

    //读取MyMain.class
    CtClass ctClass = classPool.makeClass(FileUtils.openInputStream(new File(
        "/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist/com/xycode/techlecture/asm/MyMain.class")));

    //获取foo方法(根据 方法名 + 方法签名)
    CtMethod ctMethod = ctClass.getMethod("foo", "(ID)D");

    //插入指定语句, javassit支持直接插入java代码, 而不仅仅是字节码指令, 这里的$_代表函数的返回值
    ctMethod.insertAfter("System.out.println(\"foo(\"+$1+\",\"+$2+\")=\"+$_);");

    ctClass.writeFile("/Users/xycode/IdeaProjects/techlecture/techlecture/src/test/java/com/xycode/techlecture/javassist");

生成的类文件反编译如下:

反射调用修改后的foo方法,如下:

目录
相关文章
|
安全 算法 Java
从零开发基于ASM字节码的Java代码混淆插件XHood
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施,例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低,经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响
133 1
从零开发基于ASM字节码的Java代码混淆插件XHood
|
5月前
|
Java API Android开发
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
96 0
|
5月前
|
存储 算法 Java
ASM字节码操纵框架实现AOP
ASM字节码操纵框架实现AOP
50 0
|
5月前
|
存储 算法 Java
Android 进阶——代码插桩必知必会&ASM7字节码操作
Android 进阶——代码插桩必知必会&ASM7字节码操作
241 0
|
5月前
|
Java Kotlin
ASM字节码插桩实现点击防抖
ASM字节码插桩实现点击防抖
47 0
|
监控 安全 Java
手把手带你实战 AGP 7.x ASM 字节码插桩
本文介绍了如何使用 AGP 7.0 推荐的 Transform Action API 来实现 ASM 插桩。
1327 0
手把手带你实战 AGP 7.x ASM 字节码插桩
|
存储 监控 Java
字节码插桩(三): ASM 字节码插桩(1)
字节码插桩(三): ASM 字节码插桩
254 0
字节码插桩(三): ASM 字节码插桩(1)
|
存储 算法 Java
一起来学字节码插桩:ASM Tree API
`ASM`是一个通用的`Java字节码操作和分析框架`。它可用于`修改现有类`或`直接以二进制形式动态生成类`。`ASM`提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。
281 0
|
Java API 开发工具
字节码插桩(三): ASM 字节码插桩(2)
字节码插桩(三): ASM 字节码插桩(2)
138 0
|
自然语言处理 负载均衡 Kubernetes
传统微服务框架如何无缝过渡到服务网格 ASM
让我们一起来看下传统微服务迁移到服务网格技术栈会有哪些已知问题,以及阿里云服务网格 ASM 又是如何无缝支持 SpringCloud 、Dubbo 这些服务的。
传统微服务框架如何无缝过渡到服务网格 ASM