Java 之所以能够实现“一次编译,到处运行”是因为 Java
源代码经过编译器编译后生成的是固定格式的字节码(.class
)文件,而不是特定于某个平台的本机机器代码。字节码是一种中间代码,它与特定平台无关。并且每个支持 Java
的平台都需要有相应的 JVM,负责解释和执行字节码。
Java 中使用命令 javac [options] <sourcefiles>
编译源码,一个 .java
源码文件从编译到运行的示例图:
Java 字节码结构
public class ByteCodeDemo {
private String prefix = "A";
public String getPrefix() { return prefix; }
}
对应字节码文件 ByteCodeDemo.class
根据 JVM
规范,每个 class
文件具有固定的数据结构。
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 的整体思路是:
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:(字段)
VisitAnnotation()
: 用于访问字段的注解。- 描述:表示一个字段的节点。它是 ClassNode 的一个子节点。
- 方法:
- MethodNode:(方法)
VisitLocalVariable()
: 用于访问方法的局部变量。VisitAnnotation()
: 用于访问方法的注解。Instructions
: 代表方法体中的指令列表。- 描述:表示一个方法的节点。它是 ClassNode 的一个子节点。
- 方法:
- InsnList:(有序的指令集合)
Add()
: 添加一个指令到列表中。Accept()
: 接受一个访问者,允许对指令列表进行访问。- 描述:表示一组字节码指令的列表。它通常由 MethodNode 的
Instructions
字段持有。 - 方法:
- AbstractInsnNode:(单条指令)
- 描述:表示字节码中的单个指令节点的抽象基类。
- 子类:有各种具体的指令节点,例如
VarInsnNode
、MethodInsnNode
等。
这些类和接口之间的关系形成了一个树形结构,其中 ClassNode
是根节点,MethodNode
和 FieldNode
是其直接的子节点,而 InsnList
包含在 MethodNode
中。通过这个树形结构,开发者可以方便地分析和修改字节码,而不需要直接操作字节码数组。
基于 ASM Tree API
进行 Class Transformation 处理流程
ASM Tree API
进行 Class Transformation 的流程,是利用 Core API
处理流程。
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
源码:
public class ByteCodeDemo {
private String prefix = "A";public String getPrefix() {
return prefix;
}
public void test() {
System.out.println("ByteCodeDemo#test");
}
}
字节码操作:构建 Visitor 给每个方法开始和结尾输出标识
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); }
}
}
调用方法修改字节码
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");
}
}
结果:修改后字节码文件后,对应的反编译结果
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
源文件
public class ByteCodeDemo {
private String prefix = "A"; public String getPrefix() { return prefix; } public void test() { System.out.println("ByteCodeDemo#test"); }
}
字节码操作类:在字节码文件中,增加类变量
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");
}
}
结果:修改后字节码,反编译结果
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,插入日志、性能监控等横切关注点。
- 代码生成: 动态创建类和方法,实现动态代理。
- 代码分析: 对现有代码进行静态分析。
参考
- ASM:https://asm.ow2.io/index.html
- Java ASM系列:Tree Based Class Transformation:https://blog.51cto.com/lsieun/4278784