字节码操作框架介绍与实践(以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方法,如下:

目录
相关文章
|
5月前
|
安全 算法 Java
从零开发基于ASM字节码的Java代码混淆插件XHood
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施,例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低,经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响
70 1
从零开发基于ASM字节码的Java代码混淆插件XHood
|
监控 安全 Java
手把手带你实战 AGP 7.x ASM 字节码插桩
本文介绍了如何使用 AGP 7.0 推荐的 Transform Action API 来实现 ASM 插桩。
1021 0
手把手带你实战 AGP 7.x ASM 字节码插桩
|
存储 算法 Java
一起来学字节码插桩:ASM Tree API
`ASM`是一个通用的`Java字节码操作和分析框架`。它可用于`修改现有类`或`直接以二进制形式动态生成类`。`ASM`提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。
200 0
|
Java API 开发工具
字节码插桩(三): ASM 字节码插桩(2)
字节码插桩(三): ASM 字节码插桩(2)
110 0
|
存储 监控 Java
字节码插桩(三): ASM 字节码插桩(1)
字节码插桩(三): ASM 字节码插桩
188 0
字节码插桩(三): ASM 字节码插桩(1)
|
Oracle 关系型数据库
❤️Oracle ASM加磁盘及剔盘操作❤️
❤️Oracle ASM加磁盘及剔盘操作❤️
189 0
|
监控 Java Android开发
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
298 0
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
|
监控 NoSQL Java
ASM字节码编程 | 用字节码增强技术给所有方法加上TryCatch捕获异常并输出!
最简单粗暴的可能就是硬编码在方法中,收取执行耗时以及出入参和异常信息。但这样的成本实在太大,而且有一些不可预估的风险。 可以选择切面方式做一套统一监控的组件,相对来说还是好一些的。但也需要硬编码,同时维护成本不低。 市面上对于这样的监控其实是有整套的非入侵监控方案的,比如;Google Dapper、Zipkin等都可以实现,他们都是基于探针技术非入侵的采用字节码增强的方式进行监控。
387 0
ASM字节码编程 | 用字节码增强技术给所有方法加上TryCatch捕获异常并输出!
|
存储 监控 架构师
ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时
研发代码涉及流程链路展示、每个链路测试次数、通过次数、失败次数、当时的出入参信息以及对应的代码块在当前提测分支修改记录等各项信息。最终测试在执行验证时候,分配验证渠道扫描到所有分支节点,可以清晰的看到全链路的影响。那么,这样的测试才是可以保证系统的整体质量的。
491 0
ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时
|
Java 数据库连接 Spring
ASM 字节码增强框架详解(下)
ASM是Java中比较流行的用来读写字节码的类库,用来基于字节码层面对代码进行分析和转换。 ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。 ASM可以直接产生二进制class文件,也可在类被加载入虚拟机之前动态改变类行为, ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能根据要求生成新类。目前许多框架如cglib、Hibernate、 Spring都直接或间接使用ASM操作字节码。
486 0
ASM 字节码增强框架详解(下)