作者:董子龙
前段时间一直想参照 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
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.1</version>
</dependency>
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
}
}
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
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");
}
}
作为字节码增强系列文章的开篇,只是简单的介绍了一下字节码的定义、字节码的实现方式,最后通过具体示例向大家展示了如何对字节码进行增强。再后续的文章中,会对相关框架的原理及具体应用做一个细化的总结,欢迎各位大佬的批评与指正。