Java 之所以能够实现"一次编译,到处运行"是因为 Java
源代码经过编译器编译后生成的是固定格式的字节码(.class
)文件,而不是特定于某个平台的本机机器代码。字节码是一种中间代码,它与特定平台无关。并且每个支持 Java
的平台都需要有相应的 JVM,负责解释和执行字节码。
Java 中使用命令 javac [options] <sourcefiles>
编译源码,一个 .java
源码文件从编译到运行的示例图:
Java 字节码结构
typescript
public class ByteCodeDemo {
private String prefix = "A";
public String getPrefix() {
return prefix;
}
}
对应字节码文件 ByteCodeDemo.class
根据 JVM
规范,每个 class
文件具有固定的数据结构。
ini
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 https://zhuanlan.zhihu.com/p/94498015?hmsr=toutiao.io;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
可以看到 class
文件由固定结构构成:
魔数、版本号、常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表。
字节码结构详细解释,可参考官方文档:Chapter 4. The class
File Format
字节码查看工具
这里介绍三种查看字节码命令的方式
方式一:
JDK
工具包的 bin
目录下提供的 javap
,该工具可以查看 Java
编译后的 class
文件,使用命令如下命令进行查看。
在 class
文件目录下执行 javap -c 文件名.class
,输出 class
字节码文件。
方式二:
Idea 插件 Bytecode Viewer
。在 class
文件中点击菜单 view -> Show Bytecode
插件输出字节码文件
方式三:
Idea 插件 Jclasslib Bytecode Viewer。
在 class
文件中点击菜单 view -> Show Bytecode With Jclasslib
插件输出字节码信息
此插件会分好类,对于不认识的字节码指令,可以直接跳转 JDK
官网的字节码命令网页地址。
字节码增强
字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。
以下是一些常见的 Java
字节码类库:
-
ASM (Bytecode Manipulation Framework):
- 简介:ASM 是一个轻量级的字节码操作框架,提供了生成和转换字节码的功能。它是一个强大的字节码工具,被广泛用于许多 Java 字节码操作的场景。
- 官方网站:ASM
-
Byte Buddy:
- 简介:Byte Buddy 是一个用于创建和操作字节码的库。它提供了一个高层次的 API,用于动态创建类、生成代理和拦截方法调用等。
- 官方网站:Byte Buddy
-
Javassist:
- 简介:Javassist 是一个用于在运行时编辑字节码的库。它提供了简单的 API,允许开发者在不需要事先编译的情况下修改类的结构。
- 官方网站:Javassist
-
CGlib (Code Generation Library):
- 简介:CGlib 是一个字节码生成库,它扩展了 Java 类。它常用于生成动态代理对象和拦截方法调用。
- GitHub 地址:CGlib
-
BCEL (Byte Code Engineering Library):
- 简介:BCEL 是一个用于分析、创建和修改 Java 字节码的库。它提供了许多类和方法,用于处理类文件的各个方面。
- 官方网站:BCEL
-
JBC (Java Bytecode Editor):
- 简介:JBC 是一个简单的 Java 字节码编辑器,它提供了一个图形用户界面,用于浏览、编辑和修改字节码。
- GitHub 地址:JBC
本文则介绍 ASM 字节码增强类库
ASM
ASM是一个 Java
字节码操作和解析框架。ASM 可以在类被加载入 JVM
之前动态修改已存在类行为,也可以直接生成 .class
字节码文件。ASM 提供了和其他字节码框架相似的功能,整个类包非常小,不到120KB,但其非常注重对类字节码的操作速度。这种高性能来自于它的设计模式 - 访问者模式,即通过 Reader、Visitor 和 Writer 模式。
ASM 是直接操作类字节码数据,因此其读写对象是字节码指令。
ASM API
从组成结构上可以分成两部分,一部分为 Core API
,另一部分为 Tree API
:
ASM Core API
包括asm.jar
、asm-util.jar
和asm-commons.jar
。ASM Tree API
包括asm-tree.jar
和asm-analysis.jar
。
Core API
是基础,Tree API
也是基于 Core API
构建的。
ASM Core API
ASM Core API
使用流式的方式根据字节码结构从上到下依次处理,性能很好,所以一般 ASM 增强字节码一般都使用 Core API
。
核心类:
ClassReader
:读取字节码并将其转换为内部数据结构。ClassWriter
:将内部数据结构转换回字节码,允许对字节码进行修改。ClassVisitor
:字节码访问者接口,通过它可以在访问字节码的过程中进行操作。CoreAPI
根据字节码结构从上到下依次处理,对于字节码文件中不同的区域有不同的 Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的FieldVisitor
、用于访问注解的AnnotationVisitor
等。
基于 Core API
进行 Class Transformation 处理流程
在 Core API
中,使用 ClassReader
、ClassWriter
、ClassVisitor
类进行 Class Transformation 的整体思路是:
rust
ClassReader --> ClassVisitor[1] --> ...... --> ClassVisitor[N] --> ClassWriter
ClassVisitor
是 Class Transformation 的核心操作。通过 ClassVisitor
可以访问到字节码不同区域对应的 Visitor
,通过对应的 Visitor
做相应的修改。
ASM Tree API
ASM Tree API
是 ASM 框架提供的一种基于树结构的字节码访问方式。将字节码文件读取到内存中构建树结构,通过各种 Node 类来映射字节码。与传统的基于事件的访问方式相比,Tree API
更直观,使开发者能够以树形结构的方式轻松分析和修改字节码。
ASM Tree API
包括 asm-tree.jar
和 asm-analysis.jar
。
asm-tree.jar
主要类按"包含"组织关系:
-
ClassNode: (类)
-
描述:表示一个类的节点。它是整个树结构的根节点。
-
方法:
VisitMethod()
: 用于访问类中的方法。VisitField()
: 用于访问类中的字段。Accept()
: 接受一个访问者(Visitor),允许对类进行访问。
-
-
FieldNode: (字段)
-
描述:表示一个字段的节点。它是 ClassNode 的一个子节点。
-
方法:
VisitAnnotation()
: 用于访问字段的注解。
-
-
MethodNode: (方法)
-
描述:表示一个方法的节点。它是 ClassNode 的一个子节点。
-
方法:
VisitLocalVariable()
: 用于访问方法的局部变量。VisitAnnotation()
: 用于访问方法的注解。Instructions
: 代表方法体中的指令列表。
-
-
InsnList: (有序的指令集合)
-
描述:表示一组字节码指令的列表。它通常由 MethodNode 的
Instructions
字段持有。 -
方法:
Add()
: 添加一个指令到列表中。Accept()
: 接受一个访问者,允许对指令列表进行访问。
-
-
AbstractInsnNode: (单条指令)
- 描述:表示字节码中的单个指令节点的抽象基类。
- 子类:有各种具体的指令节点,例如
VarInsnNode
、MethodInsnNode
等。
这些类和接口之间的关系形成了一个树形结构,其中 ClassNode
是根节点,MethodNode
和 FieldNode
是其直接的子节点,而 InsnList
包含在 MethodNode
中。通过这个树形结构,开发者可以方便地分析和修改字节码,而不需要直接操作字节码数组。
基于 ASM Tree API
进行 Class Transformation 处理流程
ASM Tree API
进行 Class Transformation 的流程,是利用 Core API
处理流程。
rust
ClassReader --> ClassVisitor[1] --> ... --> ClassNode[M] -->... --> ClassVisitor[N] --> ClassWriter
因为 ClassNode
类(Tree API
)是继承 ClassVistor
类(Core API
),因此两个处理流程本质一样的。
这里需要考虑三点:
- 如何利用
Core API
(ClassReader
和ClassVisitor
)转为Tree API
(ClassNode
)。 - 如何将
Tree API
(ClassNode
)转为Core API
(ClassVisitor
和ClassWriter
)。 - 如何对
ClassNode
转换。
通过下文 Demo 演示使用方式。
ASM 使用
Core API 使用 Demo
ASM 版本使用 ASM9
源码:
typescript
public class ByteCodeDemo {
private String prefix = "A";
public String getPrefix() {
return prefix;
}
public void test() {
System.out.println("ByteCodeDemo#test");
}
}
字节码操作:构建 Visitor 给每个方法开始和结尾输出标识
typescript
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class ByteCodeDemoClassVisitor extends ClassVisitor implements Opcodes {
public ByteCodeDemoClassVisitor(ClassVisitor cv) {
super(ASM9, cv);
}
// 访问 class 文件时开始执行
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
}
// 访问字节码方法区时开始执行
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// 无参构造方法,这里不增强构造方法。
if (name.equals("<init>") && mv != null) {
return mv;
}
// 构造方法处理
return new ByteCodeDemoMethodVisitor(mv);
}
private class ByteCodeDemoMethodVisitor extends MethodVisitor implements Opcodes {
public ByteCodeDemoMethodVisitor(MethodVisitor mv) {
super(ASM9, mv);
}
// 进入方法代码块开始时执行方法
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false);
}
/**
* 重写方法时就需要用 ASM 的写法,手动写入或者修改字节码。
* 通过调用 MethodVisitor 的 visitXXXXInsn() 方法就可以实现字节码的插入,XXXX 对应相应的操作码助记符类型,
* 比如 mv.visitLdcInsn("end") 对应的操作码就是ldc "end",即将字符串"end" 压入栈。
*/
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false);
}
super.visitInsn(opcode);
}
}
}
调用方法修改字节码
java
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class ByteCodeCodeGenerator {
public static void main(String[] args) throws IOException {
// 构建 ClassReader
ClassReader classReader = new ClassReader("com/shsf/demo02/asm/demo/ByteCodeDemo");
// ClassWriter.COMPUTE_MAXS:自动计算帧栈信息(操作数栈 & 局部变量表)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
// 操作字节码:串联 Visitor
ByteCodeDemoClassVisitor visitor = new ByteCodeDemoClassVisitor(classWriter);
// 接受一个访问者(Visitor),允许对类进行访问
final int parsingOptions = ClassReader.SKIP_DEBUG;
classReader.accept(visitor, parsingOptions);
// 获取最终字节码:生成 byte[]
byte[] data = classWriter.toByteArray();
// 输出
File file = new File("demo02-proj06-asm/target/classes/com/shsf/demo02/asm/demo/ByteCodeDemo.class");
System.out.println("file absolute path:" + file.getAbsolutePath());
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(data);
fileOutputStream.close();
System.out.println("success");
}
}
结果:修改后字节码文件后,对应的反编译结果
csharp
public class ByteCodeDemo {
private String prefix = "A";
public ByteCodeDemo() {
}
public String getPrefix() {
System.out.println("start");
String var10000 = this.prefix;
System.out.println("end");
return var10000;
}
public void test() {
System.out.println("start");
System.out.println("ByteCodeDemo#test");
System.out.println("end");
}
}
Tree API 使用 Demo
源文件
typescript
public class ByteCodeDemo {
private String prefix = "A";
public String getPrefix() {
return prefix;
}
public void test() {
System.out.println("ByteCodeDemo#test");
}
}
字节码操作类:在字节码文件中,增加类变量
java
public class ByteCodeTreeGenerator01 {
public static void main(String[] args) throws IOException {
// 1、构建 ClassReader
ClassReader classReader = new ClassReader(ByteCodeDemo.class.getName());
// 2、构建 ClassNode
ClassNode classNode = new ClassNode(Opcodes.ASM9);
classReader.accept(classNode, ClassReader.SKIP_DEBUG);
// 3、进行 transform:类中增加一个属性
FieldNode fieldNode = new FieldNode(
Opcodes.ACC_PUBLIC, // 表示字段的访问修饰符
"name", // 字段的名称。
"Ljava/lang/String;", // 字段的描述符,表示字段的类型。
null, // 字段的签名,用于泛型类型。
null); // 字段的初始值。
classNode.fields.add(fieldNode);
// 4、构建 ClassWriter
// ClassWriter.COMPUTE_MAXS:自动计算帧栈信息(操作数栈 & 局部变量表)
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(classWriter);
// 5、生成字节码
byte[] data = classWriter.toByteArray();
// 输出到指定文件
File file = new File("demo02-proj06-asm/target/classes/com/shsf/demo02/asm/demo/ByteCodeDemo.class");
System.out.println("file absolute path:" + file.getAbsolutePath());
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(data);
fileOutputStream.close();
System.out.println("success");
}
}
结果:修改后字节码,反编译结果
csharp
public class ByteCodeDemo {
private String prefix = "A";
public String name;
public ByteCodeDemo() {
}
public String getPrefix() {
return this.prefix;
}
public void test() {
System.out.println("ByteCodeDemo#test");
}
}
ASM 工具
ASM 直接操作字节码时,需要利用一系列 VisitXXXXInsn()
方法来写对应的助记符。
面临两个问题:
1、需要知道源代码对应的各种助记符,通过 ASM 的语法转 VisitXXXXInsn()
。
2、ASM 写字节码时,要知道如何传参。
针对这两个问题,ASM 社区提供了工具 ASM ByteCode Outline。
Idea 中直接按装此插件,可以直接把源码转为 ASM 语法格式。参考源码转换后语法,在 VisitMethod()
以及 VisitInsn()
方法中写 ASM 语法逻辑即可。
应用场景
- 字节码增强: 实现 AOP,插入日志、性能监控等横切关注点。
- 代码生成: 动态创建类和方法,实现动态代理。
- 代码分析: 对现有代码进行静态分析。
参考
- [Chapter 4. The
class
File Format] docs.oracle.com/javase/spec... - ASM:asm.ow2.io/index.html
- Java ASM系列:Tree Based Class Transformation :blog.51cto.com/lsieun/4278...
推荐阅读
招贤纳士
政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。
如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注