引言 本文是 JVM Class 文件介绍的延伸部分,简单地介绍一下字节码增强技术相关的内容,更多关于 JVM 的文章均收录于<JVM系列文章> 。
字节码技术 读到这,大家应该已经对 Class 文件字节码的结构有了比较清晰的理解。接下来,我们顺带着提一下字节码增强的相关技术。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术,一般用来实现面向切面编程 AOP。要想使用字节码增强技术,可以选择的类库有很多。我们接下来将主要介绍一下 ASM 和 Javassist。
ASM 对于需要手动操纵字节码的需求,可以使用 ASM,它可以直接生产 .class 字节码文件,也可以在类被加载入 JVM 之前动态修改类行为。ASM 的应用场景有 AOP(Cglib 就是基于 ASM)、热部署、修改其他 jar 包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。ASM 有两套 API,一套是基于访问者模式 的 Core API
。另一套 API 是 Tree API
,它将字节码中的各个结构抽象成一个树形结构,通过 Tree API
可以直接修改该树形结构。
Core API 的一大特点是不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用 Core API。 在 Core API 中有以下几个关键类:
ClassReader:用于读取已经编译好的 .class 文件。
ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
各种 Visitor 类:如上所述,CoreAPI 根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的 Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的 FieldVisitor、用于访问注解的 AnnotationVisitor 等。为了实现 AOP,重点要使用的是 MethodVisitor。
而 Tree API
解析出来的树形结构非常类似于前面提到的 jclasslib 工具展示的结果。它把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi 不同于 CoreAPI,TreeAPI 通过各种 Node 类来映射字节码的各个区域。 现在我们以 Core API 为例,介绍一下 ASM 的用法,假设我们要在如下 Test 类的基础上实现一个简单的 AOP。
1 2 3 4 5 6 7 package bbm.jvm;public class Test { public void test () { System.out.println("test" ); } }
这里我们先用 ClassReader 从 CLASS_PATH 中读取上述 Test 类的字节码,然后定义一个我们自己的 MyClassVisitor,它套在 ClassWriter 之上,只对 test 进行修改。在其函数前后分别增加一条语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package bbm.jvm;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import jdk.internal.org.objectweb.asm.ClassReader;import jdk.internal.org.objectweb.asm.ClassVisitor;import jdk.internal.org.objectweb.asm.ClassWriter;import jdk.internal.org.objectweb.asm.MethodVisitor;import jdk.internal.org.objectweb.asm.Opcodes;public class TestASM { public static void main (String[] args) throws IOException { ClassReader classReader = new ClassReader("bbm.jvm.Test" ); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new MyClassVisitor(classWriter); classReader.accept(classVisitor, ClassReader.SKIP_DEBUG); byte [] data = classWriter.toByteArray(); File f = new File("/path/to/save/Test2.class" ); if (!f.exists()) { f.createNewFile(); } FileOutputStream fout = new FileOutputStream(f); fout.write(data); fout.close(); } public static class MyClassVisitor extends ClassVisitor implements Opcodes { public MyClassVisitor (ClassVisitor cv) { super (ASM5, cv); } @Override public void visit (int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod (int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (!name.equals("<init>" ) && mv != null ) { mv = new MyMethodVisitor(mv); } return mv; } class MyMethodVisitor extends MethodVisitor implements Opcodes { public MyMethodVisitor (MethodVisitor mv) { super (Opcodes.ASM5, 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 ); } @Override public void visitInsn (int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { 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 ); } mv.visitInsn(opcode); } } } }
执行成功后,就能在 /path/to/save/Test2.class
中找到 Test2 文件,该 Class 文件反编译成 Java 后内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 package bbm.jvm;public class Test { public Test () { } public void test () { System.out.println("start" ); System.out.println("test" ); System.out.println("end" ); } }
Javassist ASM 是在指令层次上操作字节码的,从前面的例子中,我们的直观感受是在指令层次上操作字节码的框架实现起来比较繁琐。故除此之外,我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架 Javassist。
利用 Javassist 实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是 ClassPool、CtClass、CtMethod、CtField 这四个类:
CtClass(compile-time class):编译时类信息,它是一个 class 文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个 CtClass 对象,用来表示这个类文件。
ClassPool:从开发视角来看,ClassPool 是一张保存 CtClass 信息的 HashTable,key 为类名,value 为类名对应的 CtClass 对象。当我们需要对某个类进行修改时,就是通过 pool.getCtClass(“className”)方法从 pool 中获取到相应的 CtClass。
CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。
接下来,我们用 Javassist 来实现一个简单的 AOP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package bbm.jvm;import java.io.IOException;import javassist.CannotCompileException;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import javassist.NotFoundException;public class TestJavassist { public static void main (String[] args) throws IOException, NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException { ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("bbm.jvm.Test" ); CtMethod m = cc.getDeclaredMethod("test" ); m.insertBefore("{ System.out.println(\"start\"); }" ); m.insertAfter("{ System.out.println(\"end\"); }" ); Class c = cc.toClass(); cc.writeFile("/path/to/save/" ); Test h = (Test)c.newInstance(); h.test(); } }
执行上述代码,能直接打印出嵌入 AOP 之后的结果:
参考内容 [1]《实战 Java 虚拟机》 [2]《深入理解 Java 虚拟机》 [3] GC复制算法和标记-压缩算法的疑问 [4] Java中什么样的对象才能作为gc root,gc roots有哪些呢? [5] concurrent-mark-and-sweep [6] 关于 -XX:+CMSScavengeBeforeRemark,是否违背cms的设计初衷? [7] Java Hotspot G1 GC的一些关键技术 [8] Java 垃圾回收权威指北 [9] [HotSpot VM] 请教G1算法的原理 [10] [HotSpot VM] 关于incremental update与SATB的一点理解 [11] Java线程的6种状态及切换 [12] Java 8 动态类型语言Lambda表达式实现原理分析 [13] Java 8 Lambda 揭秘 [14] 字节码增强技术探索 [15] 不可不说的Java“锁”事 [16] 死磕Synchronized底层实现–概论 [17] 死磕Synchronized底层实现–偏向锁 [18] 死磕Synchronized底层实现–轻量级锁 [19] 死磕Synchronized底层实现–重量级锁