百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

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

csdh11 2025-01-29 15:14 25 浏览

作者:董子龙

前言

前段时间一直想参照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包

    
    org.ow2.asm    
    asm   
    9.1

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("") && 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包


    org.javassist
    javassist
    3.28.0-GA

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、测试类输出结果



五、总结

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

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...