草庐IT

浅谈字节码增强技术系列1-字节码增强概览

Jcloud 2023-03-28 原文

作者:董子龙

前言

前段时间一直想参照lombok的实现原理写一篇可以生成业务单据修改记录插件的专利,再查阅资料的过程中,偶然了解到了字节码增强工具-byteBuddy。但是由于当时时间紧促,所以没有深入的对该组件进行了解。其实再我们的日常开发中,字节码增强组件的身影无处不在,例如spring-aop和mybatis。本着知其然也要知其所以然的精神,我决定沉下心来,对字节码增强技术做一个深入的学习和总结,本文作为该系列的开篇,主要是对字节码做一下简单的介绍,为我们后面的深入学习打下一个好的基础。

一、字节码简述

字节码是一种中间状态的二进制文件,是由源码编译过来的,可读性没有源码的高。cpu并不能直接读取字节码,在java中,字节码需要经过JVM转译成机械码之后,cpu才能读取并运行。

使用字节码的好处:一处编译,到处运行。java就是典型的使用字节码作为中间语言,在一个地方编译了源码,拿着.class文件就可以在各种计算机运行。

二、字节码增强的使用场景

如果我们不想修改源码,但是又想加入新功能,让程序按照我们的预期去运行,可以通过编译过程和加载过程中去做相应的操作,简单来讲就是:将生成的.class文件修改或者替换称为我们需要的目标.class文件。

由于字节码增强可以在完全不侵入业务代码的情况下植入代码逻辑,所以可以用它来做一些酷酷的事,比如下面的几种常见场景:

1、动态代理

2、热部署

3、调用链跟踪埋点

4、动态插入log(性能监控)

5、测试代码覆盖率跟踪

...

三、字节码增强的实现方式

字节码工具 类创建 实现接口 方法调用 类扩展 父类方法调用 优点 缺点 常见使用 学习成本
java-proxy 支持 支持 支持 不支持 不支持 简单动态代理首选 功能有限,不支持扩展 spring-aop,MyBatis 1星
asm 支持 支持 支持 支持 支持 任意字节码插入,几乎不受限制 学习难度大,编写代码多 cglib 5星
javaassit 支持 支持 支持 支持 支持 java原始语法,字符串形式插入,写入直观 不支持jdk1.5以上的语法,如泛型,增强for Fastjson,MyBatis 2星
cglib 支持 支持 支持 支持 支持 与bytebuddy看起来差不多 正在被bytebuddy淘汰 EasyMock,jackson-databind 3星
bytebuddy 支持 支持 支持 支持 支持 支持任意维度的拦截,可以获取原始类、方法,以及代理类和全部参数 不太直观,学习理解有些成本,API非常多 SkyWalking,Mockito,Hibernate,powermock 3星

四、简单示例

AOP是我们在日常开发中常用的架构设计思想,AOP的主要的实现有cglib,Aspectj,Javassist,java proxy等。接下来,我们就以我们日常开发中会遇到的在方法执行前后打印日志为切入点,手动用字节码来实现一下AOP。

定义目标接口与实现

public class SayService{
   public void say(String str) {
      System.out.println("hello" + str); 
   }
 }

定义了类SayService,再执行say方法之前,我们会打印方法开始执行start,方法执行之后,我们会打印方法执行结束end

ASM实现AOP

4.1.1、引入jar包

<dependency>    
    <groupId>org.ow2.asm</groupId>    
    <artifactId>asm</artifactId>   
    <version>9.1</version>
</dependency>

4.1.2、AOP具体实现

public class ResourceClassVisitor extends ClassVisitor implements Opcodes {

    public ResourceClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM4, cv);
    }

    public ResourceClassVisitor(int i, ClassVisitor classVisitor) {
        super(i, classVisitor);
    }

    /**访问类基本信息*/
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        this.cv.visit(version, access, name, signature, superName, interfaces);
    }

    /**访问方法基本信息*/
    @Override
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        MethodVisitor mv = this.cv.visitMethod(access, name, desc,
                signature, exceptions);
        //假如不是构造方法,我们构建方法的访问对象(MethodVisitor)
        if (!name.equals("<init>") && mv != null) {
            mv = new ResourceClassVisitor.MyMethodVisitor((MethodVisitor)mv);
        }

        return (MethodVisitor)mv;
    }

    /**自定义方法访问对象*/
    class MyMethodVisitor extends MethodVisitor implements Opcodes {

        public MyMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM4, mv);
        }
        /**此方法会在方法执行之前执行*/
        @Override
        public void visitCode() {
            super.visitCode();
            this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            this.mv.visitLdcInsn("方法开始执行start");
            this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                    "println", "(Ljava/lang/String;)V", false);
        }
        /**对应方法体本身*/
        @Override
        public void visitInsn(int opcode) {
            //在方法return或异常之前,添加一个end输出
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
                        "Ljava/io/PrintStream;");
                this.mv.visitLdcInsn("方法执行结束end");
                this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                        "println", "(Ljava/lang/String;)V", false);
            }
            this.mv.visitInsn(opcode);
        }
    }
}
public class AopTest {

    public static void main(String[] args) throws IOException {
        //第一步:构建ClassReader对象,读取指定位置的class文件(默认是类路径-classpath)
        ClassReader classReader = new ClassReader("com/aop/SayService");
        //第二步:构建ClassWriter对象,基于此对象创建新的class文件
        //ClassWriter.COMPUTE_FRAMES 表示ASM会自动计算max stacks、max locals和stack map frame的具体内容。
        //ClassWriter.COMPUTE_MAXS 表示ASM会自动计算max stacks和max locals,但不会自动计算stack map frames。
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//推荐使用COMPUTE_FRAMES
        //第三步:构建ClassVisitor对象,此对象用于接收ClassReader对象的数据,并将数据处理后传给ClassWriter对象
        ClassVisitor classVisitor = new ResourceClassVisitor(classWriter);
        //第四步:基于ClassReader读取class信息,并将数据传递给ClassVisitor对象
        //这里的参数ClassReader.SKIP_DEBUG表示跳过一些调试信息等,ASM代码看上去就会更简洁
        //这里的参数ClassReader.SKIP_FRAMES表示跳过一些方法中的部分栈帧信息,栈帧手动计算非常复杂,所以交给系统去做吧
        //推荐用这两个参数
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
        //第五步:从ClassWriter拿到数据,并将数据写出到一个class文件中
        byte[] data = classWriter.toByteArray();
        //将字节码写入到磁盘的class文件
        File f = new File("target/classes/com/aop/SayService.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();
        SayService rs = new SayService();
        rs.say("asm");//start,handle(),end
    }
}

4.1.3、测试类输出结果

Javassist实现AOP

4.2.1、引入jar包

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

4.2.2、AOP具体实现

public class AopTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("com.aop.SayService");
        CtMethod personFly = cc.getDeclaredMethod("say");
        personFly.insertBefore("System.out.println("方法开始执行start");");
        personFly.insertAfter("System.out.println("方法执行结束end");");
        cc.toClass();
        SayService sayService = new SayService();
        sayService.say("assist");
    }
}

4.2.3、测试类输出结果

五、总结

作为字节码增强系列文章的开篇,只是简单的介绍了一下字节码的定义、字节码的实现方式,最后通过具体示例向大家展示了如何对字节码进行增强。再后续的文章中,会对相关框架的原理及具体应用做一个细化的总结,欢迎各位大佬的批评与指正。

有关浅谈字节码增强技术系列1-字节码增强概览的更多相关文章

  1. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby-on-rails - 使用一系列等级计算字母等级 - 2

    这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,

  3. Ruby - 如何将消息长度表示为 2 个二进制字节 - 2

    我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  6. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  7. 阿里云RDS——产品系列概述 - 2

    基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于

  8. ruby-on-rails - 用于门户的 Ruby 技术 - 2

    我刚刚看到whitehouse.gov正在使用drupal作为CMS和门户技术。drupal的优点之一似乎是很容易添加插件,而且编程最少,即重新发明轮子最少。这实际上正是Ruby-on-Rails的DRY理念。所以:drupal的缺点是什么?Rails或其他基于Ruby的技术有哪些不符合whitehouse.org(或其他CMS门户)门户技术的资格? 最佳答案 Whatarethedrawbacksofdrupal?对于Ruby和Rails,这确实是一个相当主观的问题。Drupal是一个可靠的内容管理选项,非常适合面向社区的站点。它

  9. ruby - Sinatra:哈希的未定义方法字节大小 - 2

    很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visitthehelpcenter.关闭9年前。我正在创建一个Sinatra应用程序,它采用上传的CSV文件并将其内容放入哈希中。当我像这样在我的app.rb中引用这个散列时:hash=extract_values(path_to_filename)我不断收到此错误消息:undefinedmethod`bytesize'forHash:0x007fc5e28f2b90#object_idfile:utils.rblocation:bytesiz

  10. ruby - 从结束值创建一系列字符串 - 2

    我使用irb。下面是我写的代码。“斧头”..“bc”我期待"ax""ay""az""ba"bb""bc"但结果只是“斧头”..“bc”我该如何纠正?谢谢。 最佳答案 >puts("ax".."bc").to_aaxayazbabbbc 关于ruby-从结束值创建一系列字符串,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7617092/

随机推荐