再序
小名来啦,上次介绍了访问者模式的适用范围与设计思路,这次接着上篇文章继续补充ASM的内容,
ASM
大家都知道对于java程序,是首先经过java编译器(如javac)将java源程序编译成.class文件,而class文件是有着严谨格式规则排版的二进制文件,每个class文件里面描述了对应的java类的所有元信息。主要包括:
- 某个特殊的数字(魔数)
- 正在使用中的类文件格式版本号
- 常量
- 访问控制标记(例如类的访问范围是public、protected还是package等等)
- 该类的类型名称
- 该类的超类
- 该类所实现的接口
- 该类拥有的字段(处于超类中的字段上方)
- 该类拥有的方法(处于超类中的方法上方)
- 属性(类级别的注解)
之前在一篇文章里盗一张有意思的图,有助于大家记住这些元数据。
接着java中的classload会在程序运行前加载这些class到内存中并生成相对就的class实例。过程为装载->连接(验证,准备,解析)->初始化。
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。没错,你没有看错,ASM可以 组装class 或 改装class 内容,这是一个很牛逼的功能。如果对class的文件格式与jvm运行程序指令非常熟悉的话,可以调用ASM的函数随心所欲的生成自己想要的class.
javap
javap命令可以解析class文件内容,方便我们分析class的构成与程序的运行过程。
使用javap首先先要把jdk的安装路径下的bin目录加入到环境变量中。方便在cmd命令窗口使用。
通常我们使用最多的就是javap -v xxx.class,如:
结果显示出class文件的主次版本,常量池数据,函数的一些元数据等等。
为什么要看这些信息呢?因为有了这么信息之后,我们会更容易理解ASM的一些接口的功能。怎么看呢?我们接着往下聊。。。。
字节码组装
ASM所有强大的功能都是基于对字节码的组装能力。首先我们先看个示例,这个示例的目的是生成如下一个非常简单的类:
public class GetterSetter { private int myInt; public int getMyInt() { return this.myInt; } public void setMyInt(int var1) { this.myInt = var1; } }
首先有一个接口提供一些常量:
interface ClassGenerator { public byte[] generateClass(); public String getGenClassName(); // Helpful constants public static final String PKG_STR = ""; public static final String INST_CTOR = "sayHello"; public static final String CL_INST_CTOR = ""; public static final String J_L_O = "java/lang/Object"; public static final String VOID_SIG = "(I)I"; }
接着就是生成字节码的过程流程了:
import org.objectweb.asm.*; public class Simple implements ClassGenerator { // Helpful constants private static final String GEN_CLASS_NAME = "GetterSetter"; private static final String GEN_CLASS_STR = PKG_STR + GEN_CLASS_NAME; @Override public String getGenClassName() { return GEN_CLASS_NAME; } @Override public byte[] generateClass() { ClassWriter cw = new ClassWriter(0); ClassAdapter cv = new ClassAdapter(cw); // Visit the class header cv.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, GEN_CLASS_STR, null, J_L_O, new String[0]); generateGetterSetter(cv); //testGenerate(cv); //generateCtor(cv); cv.visitEnd(); return cw.toByteArray(); } private void generateGetterSetter(ClassVisitor cv) { // Create the private field myInt of type int. Effectively: // private int myInt; cv.visitField(Opcodes.ACC_PRIVATE, "myInt", "I", null, 1).visitEnd(); // Create a public getter method // public int getMyInt(); MethodVisitor getterVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "getMyInt", "()I", null, null); // Get ready to start writing out the bytecode for the method getterVisitor.visitCode(); // Write ALOAD_0 bytecode (push the this reference onto stack) getterVisitor.visitVarInsn(Opcodes.ALOAD, 0); // Write the GETFIELD instruction, which uses the instance on // the stack (& consumes it) and puts the current value of the // field onto the top of the stack getterVisitor.visitFieldInsn(Opcodes.GETFIELD, GEN_CLASS_STR, "myInt", "I"); // Write IRETURN instruction - this returns an int to caller. // To be valid bytecode, stack must have only one thing on it // (which must be an int) when the method returns getterVisitor.visitInsn(Opcodes.IRETURN); // Indicate the maximum stack depth and local variables this // method requires getterVisitor.visitMaxs(1, 1); // Mark that we've reached the end of writing out the method getterVisitor.visitEnd(); // Create a setter // public void setMyInt(int i); MethodVisitor setterVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "setMyInt", "(I)V", null, null); setterVisitor.visitCode(); // Load this onto the stack setterVisitor.visitVarInsn(Opcodes.ALOAD, 0); // Load the method parameter (which is an int) onto the stack setterVisitor.visitVarInsn(Opcodes.ILOAD, 1); // Write the PUTFIELD instruction, which takes the top two // entries on the execution stack (the object instance and // the int that was passed as a parameter) and set the field // myInt to be the value of the int on top of the stack. // Consumes the top two entries from the stack setterVisitor.visitFieldInsn(Opcodes.PUTFIELD, GEN_CLASS_STR, "myInt", "I"); setterVisitor.visitInsn(Opcodes.RETURN); setterVisitor.visitMaxs(2, 2); setterVisitor.visitEnd(); }
最后将ASM生成的字节码输入到文件中保存:
public class Main { public static void main(String[] args) { Main m = new Main(); ClassGenerator cg = new Simple(); byte[] b = cg.generateClass(); try { Files.write(Paths.get("target/classes/" + ClassGenerator.PKG_STR + cg.getGenClassName() + ".class"), b, StandardOpenOption.CREATE); File file = new File(cg.getGenClassName()+".class"); FileOutputStream fout = new FileOutputStream(file); fout.write(b); fout.close(); } catch (IOException ex) { Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex); } // m.callReflexive(cg.getGenClassName(), "getMyInt"); }
最后通过反编译工具就可以看到我们生成的字节码所对应的类的内容了:
ok了,,字节码生成成功。我们就一步一步的看看如何使用ASM来生成字节码的。
- 首先我们使用javap解析一下ASM生成字节码内容(GetterSetter.class).
生成头信息
cv.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, GEN_CLASS_STR, null, J_L_O, new String[0]);
这行代码主要就是生成字节码的头信息,
第一个参数:class文件的版本号:主要用于加载此class文件的jdk的版本,asm3.0只支持jdk1.6以及jdk1.6以前的版本。
第二个参数:类的访问权限控制,如public,protect,private.
第三个参数:类名。
第四个参数:类的签名信息。
第五个参数:继承的父类
第六个参数:实现的接口
设置成员变量
cv.visitField(Opcodes.ACC_PRIVATE, "myInt", "I", null, 1).visitEnd();
这行代码主要就是设置成员变量:myInt的内容
第一个参数:访问权限
第二个参数:变量名
第三个参数:变量类型
第四个参数:变量的描述。
第五个参数:变量的签名信息
第六个参数:变量的值
生成get方法
接着生成 getMyInt 方法,理解getMyInt方法的生成我们可以使用javap先看看这个方法的信息,然后反过来看ASM的生成策略。
javap命令的结果是这样的:
其中的对应到代码是这样的:
此两图一出,ASM的函数功能便了然于目。
同样setMyInt(int var1)这个方法的生成大家也可以用同样的方法理解,就比较容易理解了。
当然了我们的目的是了解ASM的函数功能,所以以这种由结果逆向学习,当然了平常使用ASM的并不是先有字节码,这时就需要我们对字节码定义,格式,以及java指令和运行过程很熟悉。
改装class&AOP实现
Aop大家大家应该都比较熟悉,常见的AOP的实现方案有如几种:
ASM既然具有组装字节码的功能。说明肯定也可以实现在某个函数调用前后加入一些我们自己的逻辑,也就是所谓的面向切面编程喽。
举个栗子
public class Account { public void operation() { System.out.println("operation..."); //TODO real operation } }
我们要实现在这个operation()内容执行前加入一些安全性的检查操作。
接着我们实现一个MethodAdapter以便组装出一条调用安全检查函数的字节码,内容类似于这样的:
class AddSecurityCheckMethodAdapter extends MethodAdapter { public AddSecurityCheckMethodAdapter(MethodVisitor mv) { super(mv); } public void visitCode() { visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", "checkSecurity", "()V"); } }
然后我们需要实现一个ClassAdapter类,在他的组装方法的函数(visitorMethod())里找到我们要aop的方法,即Accont.operation()。内容类似于这样:
class AddSecurityCheckClassAdapter extends ClassAdapter { public AddSecurityCheckClassAdapter(ClassVisitor cv) { //Responsechain 的下一个 ClassVisitor,这里我们将传入 ClassWriter, // 负责改写后代码的输出 super(cv); } // 重写 visitMethod,访问到 "operation" 方法时, // 给出自定义 MethodVisitor,实际改写方法内容 public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions); MethodVisitor wrappedMv = mv; if (mv != null) { // 对于 "operation" 方法 if (name.equals("operation")) { // 使用自定义 MethodVisitor,实际改写方法内容 wrappedMv = new AddSecurityCheckMethodAdapter(mv); } } return wrappedMv; } }
最后使用Main函数,通过使用ClassReader,ClassAdapter,ClassWriter三个核心类的使用进行AOP的编程,内容类似于这样:
public class Generator{ public static void main(String[] args) throws Exception { ClassReader cr = new ClassReader("asm.Account"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); cr.accept(classAdapter, ClassReader.SKIP_DEBUG); byte[] data = cw.toByteArray(); File file = new File("Account.class"); FileOutputStream fout = new FileOutputStream(file); fout.write(data); fout.close(); } }
运行程序后会得到一个下面的class文件,反编译后的内容是:
这样就实现了Aop的功能。
架构与实现
组装字节码和改装字节码是ASM的核心功能点。完美实现一个字节码操作框架不仅要对class文件,jvm,以及运行机制很熟悉,还要有一个优秀的架构,有位哲学家说过:ASM处处体现着设计模式的精华。
当然在ASM中体现最为典型的还是访问者模式。在说明之前还是看看几个核心的类吧
ClassReader
ClassReader类:字节码的读取与分析引擎。它采用类似SAX的事件读取机制,每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理。
ClassWriter
ClassWriter类:它实现了ClassVisitor接口,用于拼接字节码。
ClassAdapter
实现了ClassVisitor,提供接口方法的默认实现,ClassVisitor接口:定义在读取Class字节码时会触发的事件,如类头解析完成、注解解析、字段解析、方法解析等。
当然后其中的Visitor种类有很多:ClassVisitor,MethodVisitor,FieldVisitor,AnnotationVisitor等等,他们任为访问都的接口都定义了相对应类型元素的操作方法。正符合访问者模式的设计方法。
访问者模式
我们再来回顾一下访问者模式的适用范围,想一想ASM为什么会使用这种模式。。。
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。 Visitor使得你可以将相关的操作集中起来定义在一个类中。 当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。 改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。 如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
总结&遗留问题
到这里这篇文章也快结束了,但问题还有很多。比如说class的具体内容是什么?ClassReader解析class文件的过程是怎么的?jvm运行程序时的指令与流程是什么?等等等等。