AOP 的利器:ASM 3.0 介绍(三)
ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点类似,它们都被用来遍历一些复杂的数据结构。Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程。Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。
在 ASM 中,提供了一个
Cla***eader
类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept
方法,这个方法接受一个实现了 ClassVisitor
接口的对象实例作为参数,然后依次调用 ClassVisitor
接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,Cla***eader
知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor
会产生一些子过程,比如 visitMethod
会返回一个实现 MethordVisitor
接口的实例,visitField
会返回一个实现 FieldVisitor
接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于 Cla***eader
来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 Cla***eader
类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。各个
ClassVisitor
通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。ClassAdaptor
类实现了 ClassVisitor
接口所定义的所有函数,当新建一个 ClassAdaptor
对象的时候,需要传入一个实现了 ClassVisitor
接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor
类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。每个
ClassAdaptor
类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程,对类文件进行如下操作:-
删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回
null
,而不是返回由visitMethod
方法返回的MethodVisitor
对象。class DelLoginClassAdapter extends ClassAdapter { public DelLoginClassAdapter(ClassVisitor cv) { super(cv); } public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { if (name.equals("login")) { return null; } return cv.visitMethod(access, name, desc, signature, exceptions); } }
-
修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。
class AccessClassAdapter extends ClassAdapter { public AccessClassAdapter(ClassVisitor cv) { super(cv); } public FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) { int privateAccess = Opcodes.ACC_PRIVATE; return cv.visitField(privateAccess, name, desc, signature, value); } }
-
增加新的类、方法、字段
ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 ——
ClassWriter
。它实现了 ClassVisitor
接口,而且含有一个 toByteArray()
函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后),如下例:ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); Cla***eader cla***eader = new Cla***eader(strFileName); cla***eader.accept(classAdapter, Cla***eader.SKIP_DEBUG); |
综上所述,ASM 的时序图如下:
图 4. ASM – 时序图
|
|
我们还是用上面的例子,给
Account
类加上 security check 的功能。与 proxy 编程不同,ASM 不需要将 Account
声明成接口,Account
可以仍旧是一个实现类。ASM 将直接在 Account
类上动手术,给 Account
类的 operation
方法首部加上对 SecurityChecker.checkSecurity
的调用。首先,我们将从
ClassAdapter
继承一个类。ClassAdapter
是 ASM 框架提供的一个默认类,负责沟通 Cla***eader
和 ClassWriter
。如果想要改变 Cla***eader
处读入的类,然后从 ClassWriter
处输出,可以重写相应的 ClassAdapter
函数。这里,为了改变 Account
类的 operation
方法,我们将重写 visitMethdod
方法。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); |-------10--------20--------30--------40--------50--------60--------70--------80--------9| |-------- XML error: The previous line is longer than the max of 90 characters ---------| MethodVisitor wrappedMv = mv; if (mv != null) { //对于 "operation" 方法 if (name.equals("operation")) { //使用自定义 MethodVisitor,实际改写方法内容 wrappedMv = new AddSecurityCheckMethodAdapter(mv); } } return wrappedMv; } } |
下一步就是定义一个继承自
MethodAdapter
的 AddSecurityCheckMethodAdapter
,在“operation
”方法首部插入对 SecurityChecker.checkSecurity()
的调用。class AddSecurityCheckMethodAdapter extends MethodAdapter { public AddSecurityCheckMethodAdapter(MethodVisitor mv) { super(mv); } public void visitCode() { visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", "checkSecurity", "()V"); } } |
其中,
Cla***eader
读到每个方法的首部时调用 visitCode()
,在这个重写方法里,我们用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");
插入了安全检查功能。最后,我们将集成上面定义的
ClassAdapter
,Cla***eader
和ClassWriter
产生修改后的 Account
类文件:import java.io.File; import java.io.FileOutputStream; import org.objectweb.asm.*; public class Generator{ public static void main() throws Exception { Cla***eader cr = new Cla***eader("Account"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); cr.accept(classAdapter, Cla***eader.SKIP_DEBUG); byte[] data = cw.toByteArray(); File file = new File("Account.class"); FileOutputStream fout = new FileOutputStream(file); fout.write(data); fout.close(); } } |
执行完这段程序后,我们会得到一个新的 Account.class 文件,如果我们使用下面代码:
public class Main { public static void main(String[] args) { Account account = new Account(); account.operation(); } } |
使用这个 Account,我们会得到下面的输出:
SecurityChecker.checkSecurity ... operation... |
也就是说,在
Account
原来的 operation
内容执行之前,进行了 SecurityChecker.checkSecurity()
检查。上面给出的例子是直接改造
Account
类本身的,从此 Account
类的 operation
方法必须进行 checkSecurity 检查。但事实上,我们有时仍希望保留原来的 Account
类,因此把生成类定义为原始类的子类是更符合 AOP 原则的做法。下面介绍如何将改造后的类定义为 Account
的子类 Account$EnhancedByASM
。其中主要有两项工作:- 改变 Class Description, 将其命名为
Account$EnhancedByASM
,将其父类指定为Account
。 - 改变构造函数,将其中对父类构造函数的调用转换为对
Account
构造函数的调用。
在
AddSecurityCheckClassAdapter
类中,将重写 visit
方法:public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) { String enhancedName = name + "$EnhancedByASM"; //改变类命名 enhancedSuperName = name; //改变父类,这里是”Account” super.visit(version, access, enhancedName, signature, enhancedSuperName, interfaces); } |
改进
visitMethod
方法,增加对构造函数的处理: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) { if (name.equals("operation")) { wrappedMv = new AddSecurityCheckMethodAdapter(mv); } else if (name.equals("<init>")) { wrappedMv = new ChangeToChildConstructorMethodAdapter(mv, enhancedSuperName); } } return wrappedMv; } |
这里
ChangeToChildConstructorMethodAdapter
将负责把 Account
的构造函数改造成其子类 Account$EnhancedByASM
的构造函数:class ChangeToChildConstructorMethodAdapter extends MethodAdapter { private String superClassName; public ChangeToChildConstructorMethodAdapter(MethodVisitor mv, String superClassName) { super(mv); this.superClassName = superClassName; } public void visitMethodInsn(int opcode, String owner, String name, String desc) { //调用父类的构造函数时 if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { owner = superClassName; } super.visitMethodInsn(opcode, owner, name, desc);//改写父类为superClassName } } |
最后演示一下如何在运行时产生并装入产生的
Account$EnhancedByASM
。 我们定义一个 Util
类,作为一个类工厂负责产生有安全检查的 Account
类:public class SecureAccountGenerator { private static AccountGeneratorClassLoader classLoader = new AccountGeneratorClassLoade(); private static Class secureAccountClass; public Account generateSecureAccount() throws ClassFormatError, InstantiationException, IllegalAccessException { if (null == secureAccountClass) { Cla***eader cr = new Cla***eader("Account"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); cr.accept(classAdapter, Cla***eader.SKIP_DEBUG); byte[] data = cw.toByteArray(); secureAccountClass = classLoader.defineClassFromClassFile( "Account$EnhancedByASM",data); } return (Account) secureAccountClass.newInstance(); } private static class AccountGeneratorClassLoader extends ClassLoader { public Class defineClassFromClassFile(String className, byte[] classFile) throws ClassFormatError { return defineClass("Account$EnhancedByASM", classFile, 0, classFile.length()); |-------10--------20--------30--------40--------50--------60--------70--------80--------9| |-------- XML error: The previous line is longer than the max of 90 characters ---------| } } } |
静态方法
SecureAccountGenerator.generateSecureAccount()
在运行时动态生成一个加上了安全检查的 Account
子类。著名的 Hibernate 和 Spring 框架,就是使用这种技术实现了 AOP 的“无损注入”。
|
|
最后,我们比较一下 ASM 和其他实现 AOP 的底层技术:
表 1. AOP 底层技术比较
AOP 底层技术 | 功能 | 性能 | 面向接口编程 | 编程难度 |
---|---|---|---|---|
直接改写 class 文件 | 完全控制类 | 无明显性能代价 | 不要求 | 高,要求对 class 文件结构和 Java 字节码有深刻了解 |
JDK Instrument | 完全控制类 | 无论是否改写,每个类装入时都要执行hook程序 | 不要求 | 高,要求对 class 文件结构和 Java 字节码有深刻了解 |
JDK Proxy | 只能改写 method | 反射引入性能代价 | 要求 | 低 |
ASM | 几乎能完全控制类 | 无明显性能代价 | 不要求 | 中,能操纵需要改写部分的 Java 字节码 |
转载于:https://blog.51cto.com/77857/149645