浅谈字节码增强技术系列1-字节码增强概览
作者:董子龙
前言
前段时间一直想参照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、测试类输出结果
五、总结
作为字节码增强系列文章的开篇,只是简单的介绍了一下字节码的定义、字节码的实现方式,最后通过具体示例向大家展示了如何对字节码进行增强。再后续的文章中,会对相关框架的原理及具体应用做一个细化的总结,欢迎各位大佬的批评与指正。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
鸿蒙赋能体系问世,移动生态格局将巨变!
在本次 HDC 大会期间,华为发布了全新的七大鸿蒙开发工具,HarmonyOS 系统迎来了全新升级。 为了解决目前移动应用开发生态普遍存在的入门门槛高、技术交流渠道窄和应用多端适配工作繁琐的问题,华为开发者联盟也推出了一系列举措赋能开发者,力争为独立开发者带来全方位、一站式的助力提升。 那么现在就请跟随笔者的脚步,一起来探索、分析华为开发者联盟的一系列举措,究竟能够为当下开发者的技术生涯带来哪些方面的提升和改变。 当下开发者的困境 尽管我国有着全球领先的移动互联网市场体系,但是不可否认,支撑这套商业体系的底层技术生态圈却长期处于空中楼阁的状态。 除了受限于时代发展的原因,我国编程技术生态的资源和内容短缺,也对这一现状有着重要的影响。 首先,虽然如今中文互联网上的编程课程多如牛毛,但是实际真正能够起到培养软件工程师作用的课程寥寥无几。 不仅因为视频制作者水平参差不齐,导致编程课程的质量难以保证,因此误导了一大批初学编程的小白。 而且课程的时效性往往也得不到保证,毕竟编程技术的迭代突飞猛进,每一代语法可能都会出现极大的变化。 其次,众所周知如今移动互联网市场已经从蓝海变成了红海,市场竞争程...
- 下一篇
【敏捷研发系列】前端DevOps流水线实践
作者:胡骏 一、背景现状 软件开发从传统的瀑布流方式到敏捷开发,将软件交付过程中开发和测试形成快速的迭代交付,但在软件交付客户之前或者使用过程中,还包括集成、部署、运维等环节需要进一步优化交付效率。因此Devops的产生将敏捷的相关理念扩展到运维侧,从而将产品、设计、开发、测试、运维团队更紧密的结合在一起。而从交付给客户产品视角看,前端研发通常又是在整个产品设计开发链条的最终节点,意味着前端团队受到上游变更的影响是最大的,并且从经营理念效率出发,提升前端交付效率是至关重要的。那么如何提升交付效率呢,主要面临以下问题: 交付效率: 1.敏态需求增加,即迭代性工作增加:软件开发从传统的瀑布流方式到敏捷开发,再到现在对敏捷开发提出了更高的要求。近些年项目的迭代性需求不断递增,这就要求前端开发者能够具备从开发到测试并且快速发布上线的能力,也需要团队完成由稳态到敏态的转变。 2.前端研发效能瓶颈,达成双周交付面临挑战:敏捷迭代过程中,研发周期缩减并行需求增加,研发团队难以做到开发到测试和上线的过程中,全方位保证代码的高质量输出。 3.研发过程中不必要的浪费降低交付效率:重复劳动、过度沟通、环节等...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Hadoop3单机部署,实现最简伪集群
- CentOS关闭SELinux安全模块
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7