浅谈字节码增强技术系列1-字节码增强概览
zhezhongyun 2025-07-24 23:18 41 浏览
作者:董子龙
前言
前段时间一直想参照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、测试类输出结果
五、总结
作为字节码增强系列文章的开篇,只是简单的介绍了一下字节码的定义、字节码的实现方式,最后通过具体示例向大家展示了如何对字节码进行增强。再后续的文章中,会对相关框架的原理及具体应用做一个细化的总结,欢迎各位大佬的批评与指正。
相关推荐
- Python入门学习记录之一:变量_python怎么用变量
-
写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...
- python变量命名规则——来自小白的总结
-
python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...
- Python入门学习教程:第 2 章 变量与数据类型
-
2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...
- 绘制学术论文中的“三线表”具体指导
-
在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...
- Python基础语法知识--变量和数据类型
-
学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...
- 一文搞懂 Python 中的所有标点符号
-
反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...
- Python变量类型和运算符_python中变量的含义
-
别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...
- 从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序
-
在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...
- Python中下划线 ‘_’ 的用法,你知道几种
-
Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...
- 解锁Shell编程:变量_shell $变量
-
引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...
- 一文学会Python的变量命名规则!_python的变量命名有哪些要求
-
目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...
- 更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for
-
src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...
- C++第五课:变量的命名规则_c++中变量的命名规则
-
变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....
- Rust编程-核心篇-不安全编程_rust安全性
-
Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...
- 探秘 Python 内存管理:背后的神奇机制
-
在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- opacity 属性 (32)
- transition 属性 (33)
- 1-1. 变量声明 (31)
