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

目录
相关文章
|
7月前
|
运维 Kubernetes 网络协议
基于虚拟服务配置的渐进式迁移实践:Istio集群至ASM集群的平滑切换
本文介绍了从Istio+k8s环境迁移到阿里云ASM+ACK环境的渐进式方法,通过配置虚拟服务和入口服务实现新老集群间的服务调用与流量转发,确保业务连续性与平滑迁移
763 132
|
安全 算法 Java
从零开发基于ASM字节码的Java代码混淆插件XHood
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施,例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低,经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响
268 1
从零开发基于ASM字节码的Java代码混淆插件XHood
|
运维 负载均衡 监控
探索微服务架构下的服务网格(Service Mesh)实践之路
【8月更文挑战第30天】 在当今日益复杂的分布式系统中,微服务架构已成为众多企业解决系统扩展与维护难题的利器。然而,随着服务的不断增多和网络交互的复杂性提升,传统的微服务管理方式开始显得力不从心。服务网格(Service Mesh)作为一种新兴的解决方案,旨在通过提供应用层的网络基础设施来简化服务间通讯,并增强系统的可观察性和安全性。本文将分享我在采用服务网格技术过程中的经验与思考,探讨如何在现代云原生环境中有效地实施服务网格,以及它给开发和运维带来的变革。
|
负载均衡 测试技术 网络安全
阿里云服务网格ASM多集群实践(一)多集群管理概述
服务网格多集群管理网络打通和部署模式的多种最佳实践
|
Cloud Native 测试技术 开发者
阿里云服务网格ASM多集群实践(二):高效按需的应用多环境部署与全链路灰度发布
介绍服务网格ASM提出的一种多集群部署下的多环境部署与全链路灰度发布解决方案。
|
监控 负载均衡 数据安全/隐私保护
探索微服务架构下的服务网格(Service Mesh)实践
【5月更文挑战第6天】 在现代软件工程的复杂多变的开发环境中,微服务架构已成为构建、部署和扩展应用的一种流行方式。随着微服务架构的普及,服务网格(Service Mesh)作为一种新兴技术范式,旨在提供一种透明且高效的方式来管理微服务间的通讯。本文将深入探讨服务网格的核心概念、它在微服务架构中的作用以及如何在实际项目中落地实施服务网格。通过剖析服务网格的关键组件及其与现有系统的协同工作方式,我们揭示了服务网格提高系统可观察性、安全性和可操作性的内在机制。此外,文章还将分享一些实践中的挑战和应对策略,为开发者和企业决策者提供实用的参考。
|
运维 监控 负载均衡
探索微服务架构下的服务网格(Service Mesh)实践之路
【4月更文挑战第30天】 在现代云计算的大背景下,微服务架构以其灵活性和可扩展性成为众多企业转型的首选。然而,随着服务的激增和网络交互的复杂化,传统的服务通信模式已无法满足需求,服务网格(Service Mesh)应运而生。本文通过分析服务网格的核心组件、运作机制以及在企业中的实际应用案例,探讨了服务网格在微服务架构中的关键作用及其带来的变革,同时提出了实施过程中面临的挑战和解决策略。
|
Java API Android开发
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
409 0
|
存储 算法 Java
ASM字节码操纵框架实现AOP
ASM字节码操纵框架实现AOP
153 0
|
存储 算法 Java
Android 进阶——代码插桩必知必会&ASM7字节码操作
Android 进阶——代码插桩必知必会&ASM7字节码操作
919 0