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

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

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

作者:董子龙

前言

前段时间一直想参照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、测试类输出结果



五、总结

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

相关推荐

知名软件变“木马”:2小时感染10万电脑

近日,腾讯电脑管家监测发现,一款通过“驱动人生”升级通道,并同时利用“永恒之蓝”高危漏洞传播的木马突然爆发,仅2个小时受攻击用户就高达10万。腾讯电脑管家可精准拦截该病毒攻击,管家团队也将持续跟踪该款...

腾讯电脑管家发布病毒预警:“驱动人生木马”爆发,2小时感染10万台电脑

新华网天津12月15日电(记者周润健)腾讯电脑管家15日紧急发布病毒预警,14日下午,腾讯电脑管家监测发现,一款通过“驱动人生”升级通道,并同时利用“永恒之蓝”高危漏洞传播的木马突然爆发,仅2个小时受...

全新“撒旦”勒索病毒来袭 瑞星推出独家解密工具

新华社北京7月26日电瑞星威胁情报平台近日发现多起国内用户感染“撒旦”勒索病毒事件。据瑞星安全研究人员介绍,该病毒运行后会加密受害者计算机文件,加密完成后会用中英韩三国语言索取1个比特币作为赎金,并...

新勒索病毒“WannaCry”疯狂来袭 乌克兰副总理电脑中招

据外媒报道,从6月27日开始,一种新勒索病毒再次疯狂来袭,已席卷欧洲多个国家,连乌克兰副总理的电脑都已中招。报道称,这轮病毒足以与五月席卷全球的勒索病毒“WannaCry”的攻击性相提并论。该病毒代号...

蠕虫病毒利用“永恒之蓝”漏洞传播 单位局域网受威胁最大

日前,火绒安全团队通过“火绒威胁情报系统”发现蠕虫病毒“Worm/Sharp”正在全网传播,其中在政府、企业、学校、医院等单位的局域网具有非常强的传播能力。该病毒通过“永恒之蓝”漏洞、多个电脑常用端口...

新病毒爆发:利用“永恒之蓝”传播,2小时感染10万台电脑挖矿

驱动人生发布的声明。据腾讯安全专家介绍,通过追溯病毒传播链发现,该病毒自12月14日约14点,利用“驱动人生”、“人生日历”等软件最早开始传播,另有约30%的传播通过“永恒之蓝”漏洞在局域网内进行主动...

逍遥安卓模拟器定制手游电脑版 手机电脑账号完全互通

从今年起大量回合制端游转向手游方向,无论是《梦幻西游》、《大话西游》、《神武》还是刚刚发行的《问道》手游,都是非常重度需要大量时间来做任务挂机升级的游戏。很多人习惯了端游的时候一个电脑可以多开的玩法,...

安卓模拟器绿色U盘移动版 公司玩游戏无痕迹

安卓模拟器已经不稀奇了!随着安卓手游的盛行,特别是《梦幻西游手游》之类的重度手游发布,玩手游花的时间也越来越多。用手机玩这些游戏存在着屏幕小、点量少、费流量还有容易被电话打断,在电脑上用安卓模拟器玩游...

苹果推出 iCloud 照片和视频转移服务:可转移至谷歌相册

IT之家3月4日消息据MacRumors今日报道,苹果公司本周推出了一项新服务,帮助iCloud用户方便快捷的将其存储的照片和视频转移到谷歌照片上。苹果在其支持文档中表示,用户可以登...

NAS PK台,4核带m.2的威联通TS-264C vs 双核TS-462C

因为618年中大促看到威联通TS-264C和TS-462C这两机型售价差不多,就做个比较以供参考。毕竟作为自2007年以来就一直卖威联通的NAS老油来说,对威联通各NAS机型的识别还是相当全面的,对不...

前端学AI(七):构造 RAG 系统评估测试数据集

引言在基于DeepSeek+Chroma+LangChain开发一个简单RAG系统...

惨重教训!调查显示挪威“英斯塔”号宙斯盾舰撞油轮后本不必“丧命”

最新公布的调查报告显示,挪威皇家海军“英斯塔”号护卫舰2018年11月与油轮相撞后,如果其舰员接受了更好的损管训练,并且对舰艇的稳定特性更加熟悉的话,这艘宙斯盾型战舰本来是可以挽救的。↑挪威“英斯塔”...

「必买」盘点2021年男人们的败家清单,越“败”越香

心里总想买点啥?看看《必买》,全网最有料的场景种草指南。草原割不尽,春风吹又生。在过去的2021年,不断被各种数码产品种草,一直在买买买,剁手不停。大部分产品都经过详细的对比做足了功课,也有部分是一时...

实现浏览器播放rtsp视频流的解决方案

有同学问道:需要实时播放摄像头rtsp视频流,而浏览器不能直接播放,怎样解决?实现这个需求可以通过插件或者转码来实现。要实现这个目的,可以采用的方案非常得多,有商业的也有开源的,这里主要列举一些开源的...

ISO9000你知道多少?

1ISO9000族标准是什么?ISO9000族标准是指由国际标准化质量管理和质量保证技术委员会(ISO/TC176)制订的所有国际标准。ISO9000族标准可帮助各种类型和规模的组织实施并有效运行质...