ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者加强既有类的功能。ASM 能够直接产生二进制 class 文件,也能够在类被加载入 Java 虚拟机以前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的全部元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,可以改变类行为,分析类信息,甚至可以根据用户要求生成新类。java
与 BCEL 和 SERL 不一样,ASM 提供了更为现代的编程模型。对于 ASM 来讲,Java class 被描述为一棵树;使用 “Visitor” 模式遍历整个二进制结构;事件驱动的处理方式使得用户只须要关注于对其编程有意义的部分,而没必要了解 Java 类文件格式的全部细节:ASM 框架提供了默认的 “response taker”处理这一切。web
动态生成 Java 类与 AOP 密切相关的。AOP 的初衷在于软件设计世界中存在这么一类代码,零散而又耦合:零散是因为一些公有的功能(诸如著名的 log 例子)分散在全部模块之中;同时改变 log 功能又会影响到全部的模块。出现这样的缺陷,很大程度上是因为传统的 面向对象编程注重以继承关系为表明的“纵向”关系,而对于拥有相同功能或者说方面 (Aspect)的模块之间的“横向”关系不能很好地表达。例如,目前有一个既有的银行管理系统,包括 Bank、Customer、Account、Invoice 等对象,如今要加入一个安全检查模块, 对已有类的全部操做以前都必须进行一次安全检查。算法
图 1. ASM – AOP编程
然而 Bank、Customer、Account、Invoice 是表明不一样的事务,派生自不一样的父类,很难在高层上加入关于 Security Checker 的共有功能。对于没有多继承的 Java 来讲,更是如此。传统的解决方案是使用 Decorator 模式,它能够在必定程度上改善耦合,而功能仍旧是分散的 —— 每一个须要 Security Checker 的类都必需要派生一个 Decorator,每一个须要 Security Checker 的方法都要被包装(wrap)。下面咱们以 Account
类为例看一下 Decorator:设计模式
首先,咱们有一个 SecurityChecker
类,其静态方法 checkSecurity
执行安全检查功能:数组
public class SecurityChecker { public static void checkSecurity() { System.out.println("SecurityChecker.checkSecurity ..."); //TODO real security check } }
另外一个是 Account
类:安全
public class Account { public void operation() { System.out.println("operation..."); //TODO real operation } }
若想对 operation
加入对 SecurityCheck.checkSecurity()
调用,标准的 Decorator 须要先定义一个 Account
类的接口:数据结构
public interface Account { void operation(); }
而后把原来的 Account
类定义为一个实现类:架构
public class AccountImpl extends Account{ public void operation() { System.out.println("operation..."); //TODO real operation } }
定义一个 Account
类的 Decorator,并包装 operation
方法:app
public class AccountWithSecurityCheck implements Account { private Account account; public AccountWithSecurityCheck (Account account) { this.account = account; } public void operation() { SecurityChecker.checkSecurity(); account.operation(); } }
在这个简单的例子里,改造一个类的一个方法还好,若是是变更整个模块,Decorator 很快就会演化成另外一个噩梦。动态改变 Java 类就是要解决 AOP 的问题,提供一种获得系统支持的可编程的方法,自动化地生成或者加强 Java 代码。这种技术已经普遍应用于最新的 Java 框架内,如 Hibernate,Spring 等。
最直接的改造 Java 类的方法莫过于直接改写 class 文件。Java 规范详细说明了 class 文件的格式,直接编辑字节码确实能够改变 Java 类的行为。直到今天,还有一些 Java 高手们使用最原始的工具,如 UltraEdit 这样的编辑器对 class 文件动手术。是的,这是最直接的方法,可是要求使用者对 Java class 文件的格式了熟于心:当心地推算出想改造的函数相对文件首部的偏移量,同时从新计算 class 文件的校验码以经过 Java 虚拟机的安全机制。
Java 5 中提供的 Instrument 包也能够提供相似的功能:启动时往 Java 虚拟机中挂上一个用户定义的 hook 程序,能够在装入特定类的时候改变特定类的字节码,从而改变该类的行为。可是其缺点也是明显的:
ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
,仍是 Instrument.redefineClasses(ClassDefinition[] definitions)
,都必须提供新 Java 类的字节码。也就是说,同直接改写 class 文件同样,使用 Instrument 也必须了解想改造的方法相对类首部的偏移量,才能在适当的位置上插入新的代码。尽管 Instrument 能够改造类,但事实上,Instrument 更适用于监控和控制虚拟机的行为。
一种比较理想且流行的方法是使用 java.lang.ref.proxy
。咱们仍旧使用上面的例子,给 Account
类加上 checkSecurity 功能 :
首先,Proxy 编程是面向接口的。下面咱们会看到,Proxy 并不负责实例化对象,和 Decorator 模式同样,要把 Account
定义成一个接口,而后在 AccountImpl
里实现 Account
接口,接着实现一个 InvocationHandler
Account
方法被调用的时候,虚拟机都会实际调用这个InvocationHandler
的 invoke
方法:
class SecurityProxyInvocationHandler implements InvocationHandler { private Object proxyedObject; public SecurityProxyInvocationHandler(Object o) { proxyedObject = o; } public Object invoke(Object object, Method method, Object[] arguments) throws Throwable { if (object instanceof Account && method.getName().equals("opertaion")) { SecurityChecker.checkSecurity(); } return method.invoke(proxyedObject, arguments); } }
最后,在应用程序中指定 InvocationHandler
生成代理对象:
public static void main(String[] args) { Account account = (Account) Proxy.newProxyInstance( Account.class.getClassLoader(), new Class[] { Account.class }, new SecurityProxyInvocationHandler(new AccountImpl()) ); account.function(); }
其不足之处在于:
Proxy.newProxyInstance
生成的是实现 Account
接口的对象而不是 AccountImpl
的子类。这对于软件架构设计,尤为对于既有软件系统是有必定掣肘的。ASM 可以经过改造既有类,直接生成须要的代码。加强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不一样,不须要为加强代码而新定义一个接口,生成的代码能够覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至能够在应用程序的类框架中拥有本身的位置,派生本身的子类。
相比于其余流行的 Java 字节码操纵工具,ASM 更小更快。ASM 具备相似于 BCEL 或者 SERP 的功能,而只有 33k 大小,然后者分别有 350k 和 150k。同时,一样类转换的负载,若是 ASM 是 60% 的话,BCEL 须要 700%,而 SERP 须要 1100% 或者更多。
ASM 已经被普遍应用于一系列 Java 项目:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也经过 cglib,另外一个更高层一些的自动代码生成工具使用了 ASM。
所谓 Java 类文件,就是一般用 javac 编译器产生的 .class 文件。这些文件具备严格定义的格式。为了更好的理解 ASM,首先对 Java 类文件格式做一点简单的介绍。Java 源文件通过 javac 编译器编译以后,将会生成对应的二进制文件(以下图所示)。每一个合法的 Java 类文件都具有精确的定义,而正是这种精确的定义,才使得 Java 虚拟机得以正确读取和解释全部的 Java 类文件。
图 2. ASM – Javac 流程
Java 类文件是 8 位字节的二进制流。数据项按顺序存储在 class 文件中,相邻的项之间没有间隔,这使得 class 文件变得紧凑,减小存储空间。在 Java 类文件中包含了许多大小不一样的项,因为每一项的结构都有严格规定,这使得 class 文件可以从头至尾被顺利地解析。下面让咱们来看一下 Java 类文件的内部结构,以便对此有个大体的认识。
例如,一个最简单的 Hello World 程序:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world"); } }
通过 javac 编译后,获得的类文件大体是:
图 3. ASM – Java 类文件
从上图中能够看到,一个 Java 类文件大体能够归为 10 个项:
事实上,使用 ASM 动态生成类,不须要像早年的 class hacker 同样,熟知 class 文件的每一段,以及它们的功能、长度、偏移量以及编码方式。ASM 会给咱们照顾好这一切的,咱们只要告诉 ASM 要改动什么就能够了 —— 固然,咱们首先得知道要改什么:对类文件格式了解的越多,咱们就能更好地使用 ASM 这个利器。
ASM 经过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程当中对字节码进行修改。所谓的 Push 模型相似于简单的 Visitor 设计模式,由于须要处理字节码结构是固定的,因此不须要专门抽象出一种 Vistable 接口,而只须要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点相似,它们都被用来遍历一些复杂的数据结构。Visitor 至关于用户派出的表明,深刻到算法内部,由算法安排访问行程。Visitor 表明能够更换,但对算法流程没法干涉,所以是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。
在 ASM 中,提供了一个 ClassReader
类,这个类能够直接由字节数组或由 class 文件间接的得到字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept
方法,这个方法接受一个实现了 ClassVisitor
接口的对象实例做为参数,而后依次调用ClassVisitor
接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的前后,所谓 visit 事件是指对各类不一样 visit 函数的调用,ClassReader
知道如何调用各类 visit 函数。在这个过程当中用户没法对操做进行干涉,因此遍历的算法是肯定的,用户能够作的是提供不一样的 Visitor 来对字节码树进行不一样的修改。ClassVisitor
会产生一些子过程,好比 visitMethod
会返回一个实现 MethordVisitor
接口的实例,visitField
会返回一个实现 FieldVisitor
接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。所以对于ClassReader
来讲,其内部顺序访问是有必定要求的。实际上用户还能够不经过 ClassReader
类,自行手工控制这个流程,只要按照必定的顺序,各个 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); ClassReader classReader = new ClassReader(strFileName); classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
综上所述,ASM 的时序图以下:
图 4. ASM – 时序图
咱们仍是用上面的例子,给 Account
类加上 security check 的功能。与 proxy 编程不一样,ASM 不须要将 Account
声明成接口,Account
能够仍旧是一个实现类。ASM 将直接在 Account
类上动手术,给 Account
类的 operation
方法首部加上对SecurityChecker.checkSecurity
的调用。
首先,咱们将从 ClassAdapter
继承一个类。ClassAdapter
是 ASM 框架提供的一个默认类,负责沟通 ClassReader
和 ClassWriter
。若是想要改变 ClassReader
处读入的类,而后从 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); 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"); } }
其中,ClassReader
读到每一个方法的首部时调用 visitCode()
,在这个重写方法里,咱们用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");
插入了安全检查功能。
最后,咱们将集成上面定义的 ClassAdapter
,ClassReader
和 ClassWriter
产生修改后的 Account
类文件 :
import java.io.File; import java.io.FileOutputStream; import org.objectweb.asm.*; public class Generator{ public static void main() throws Exception { ClassReader cr = new ClassReader("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(); } }
执行完这段程序后,咱们会获得一个新的 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
。其中主要有两项工做 :
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) { ClassReader cr = new ClassReader("Account"); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); cr.accept(classAdapter, ClassReader.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()); } } }
静态方法 SecureAccountGenerator.generateSecureAccount()
在运行时动态生成一个加上了安全检查的 Account
子类。著名的 Hibernate 和 Spring 框架,就是使用这种技术实现了 AOP 的“无损注入”。