1. 字节码
1.1 什么是字节码
Java之所以可以"一次编译,到处运行",一是因为JVM针对各种平台和操作系统都进行了定制,对开发者屏蔽了底层细节。二是因为无论在任何平台都会编译生成固定格式的字节码(.class)文件供JVM使用,不同平台上的JVM虚拟机都可以载入和执行同一种和平台无关的字节码。由此可见字节码对于Java生态的重要性。之所以被称为字节码,是因为字节码文件由16进制值组成,JVM以字节为单位进行读取。在Java中一般使用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如下图所示。
1.2 字节码结构
.java文件通过javac编译后将得到.class文件,如编写一个简单的ByteCodeDemo类,如下图的左侧部分。编译后生成ByteCodeDemo.class文件,打开后是一堆16进制数,按字节为单位分割展示如下图右侧部分展示。
JVM规范要求每一个字节码文件都需要由10个部分按固定顺序组成,整体结果如下图所示。
2. ASM
ASM是一个Java字节码操控框架。它可以被用来动态生成类或者增强既有类的功能。ASM可以直接产生二进制的class文件,也可以在类被加载到Java虚拟机之前改变类行为。ASM的应用场景有AOP(cglib基于ASM)、热部署、修改其它jar包中的类等。ASM修改字节码流程如下图所示。
2.1 ASM核心API
2.1.1 ClassReader
用于读取已经编译好的.class文件;
2.1.2 ClassWriter
用于重新构建编译后的类,如修改类名、属性及方法等;
2.1.3 ClassVistor
ASM内部采用访问者模式根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor。
- 访问类文件时,会回调ClassVistor的visit方法;
- 访问类注解时,会回调ClassVistor的visitAnnotation方法;
- 访问类成员时,会回调ClassVistor的visitField方法;
- 访问类方法时,会回调ClassVistor的visitMethod方法;
- .......
当访问不同区域时会回调相应方法,该方法会返回一个对应的字节码操作对象。通过修改这个对象就可以修改class文件相应结构对应的内容。最后将这个对象的字节码内容覆盖原先的.class文件就可以实现字节码的切入。
2.2 ASM实现AOP示例
代码示例如下,原本Base的process方法只输出一行"process"。
目标:我们期望通过字节码增强后,在方法执行前输出"start",在方法执行后输出"end"。
public class Base {
public void process() {
System.out.println("process");
}
}
要实现在process方法前后插入输出代码,需要有以下步骤:
- (1)读取Base.class文件,可以通过ClassReader进行class文件的读取;
- (2)构造System.out.println(String)的字节码,通过ClassVisitor将额外的字节码嵌入合适地方;
- (3)在ClassVisitor处理完成后,由ClassWriter将新的字节码替换掉旧的字节码;
基本实现代码参考如下,SimpleGenerator类定义了ClassReader和ClassWriter对象。其中ClassReader负责读取字节码,然后交给ClassVisitor类处理,处理完成后由ClassWriter完成字节码文件替换。
public class SimpleGenerator {
public static void main(String[] args) throws Exception {
//读取类文件
ClassReader classReader = new ClassReader("com.example.aop.asm.Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理,通过classVisitor修改类
ClassVisitor classVisitor = new SimpleClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//保存新的字节码文件
File file = new File("target/classes/com/example/aop/asm/Base.class");
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(data);
outputStream.close();
System.out.println("generator new class file success.");
}
}
SimpleClassVisitor类继承ClassVisitor类。ClassVisitor中各方法按以下顺序调用,这个顺序在ClassVisitor的java doc中也有说明。
[
visitSource
] [visitOuterClass
] (visitAnnotation
|visitTypeAnnotation
|visitAttribute
)* (visitInnerClass
|visitField
|visitMethod
)*visitEnd
.
其中visitMethod在访问方法时调用,我们可以通过扩展MethodVisitor方法来实现增强逻辑。类似地,MethodVisitor也需要按顺序调用其方法。
(
visitParameter
)* [visitAnnotationDefault
] (visitAnnotation
|visitAnnotableParameterCount
|visitParameterAnnotation
visitTypeAnnotation
|visitAttribute
)* [visitCode
(visitFrame
|visitXInsn
|visitLabel
|visitInsnAnnotation
|visitTryCatchBlock
|visitTryCatchAnnotation
|visitLocalVariable
|visitLocalVariableAnnotation
|visitLineNumber
)*visitMaxs
]visitEnd
.
public class SimpleClassVisitor extends ClassVisitor {
public SimpleClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
// visitMethod在访问类方法时回调,可以获知当前访问到方法信息
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
// 不需要增强Base类的构造方法
if (mv != null && !name.equals("<init>")) {
// 通过定制MethodVisitor,替换原来的MethodVisitor对象来实现方法改写
mv = new SimpleMethodVisitor(mv);
}
return mv;
}
/**
* 定制方法visitor
*/
class SimpleMethodVisitor extends MethodVisitor {
public SimpleMethodVisitor(MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
}
// visitCode方法在ASM开始访问方法的Code区时回调
@Override
public void visitCode() {
super.visitCode();
// System.out.println("start")对应的字节码,在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访问到无参数指令时,都会调用visitInsn方法
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
// System.out.println("end")对应的字节码,在visitInsn方法返回前添加
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);
}
}
}
运行SimpleGenerator类的main方法即可完成对Base类的字节码增强。增强后的class文件通过反编译后结果如下图所示。可以看到对应的代码已经改变,在其前后增加了"start"和"end"的输出。
可以通过ASM ByteCode Outline 插件很方便地实现源码到字节码的映射。
3. 参考资料
欢迎关注我的公众号~