什么是ASM?
ASM是一个字节码操控框架。
它能被用来动态生成类,或者增强既有类的功能。
ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类的行为。
与BCEL和SERL不同,ASM提供了更为现代的编程模型。
对于ASM来说,Java class被描述为一棵树;使用“Visitor”模式遍历整个二进制结构;采用事件驱动的处理方式,用户只需要关注于对其编程有意义的部分,而不必了解Java类文件格式的所有细节;
ASM框架提供了默认的“response taker”处理上述所说的一切
。
为什么要动态生成Java类?
动态生成Java类与AOP是密切相关的。
AOP的初衷是在于,软件设计世界中存在着这么一类代码,零散而又耦合
。
零散是由于一些公有的功能(比如log)分散在所有的模块之中,同时改变log的功能又会影响到所有的模块。
出现这样的缺陷,很大程度上是由于传统的面向对象编程注重以继承关系为代表的纵向
关系,而对于拥有相同功能或者说方面(Aspect)的模块之间的横向
关系不能很好的表达。
例如:目前有一个现有的银行管理系统,它包括Bank, Customer, Account, Invoice等对象,现在要加入一个安全检查模块,对已有类的所有操作之前都必须进行一次安全检查。
而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方法:
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代码。
PS : Java类的文件概述
为什么选择ASM?
最直接的改造Java类的方法,莫过于直接改写class文件。
Java规范详细说明了class文件格式,通过直接编辑字节码,确实是一个可以改变Java类的方法。
但是,这得要求使用者对Java class文件的格式了熟于心,我们得小心地推算出,想改造的函数相对于文件首部的偏移量,同时重新计算class文件的检验码,以通过Java虚拟机的安全机制。
看到这里不是有了一种写汇编的感觉了~~~
Java Instrument包
Java 5之后提供的Instrument包,它包含了类似的功能:在启动时往Java虚拟机中挂上一个用户自定义的hook程序,可以在装入特定类的时候改变特定类的字节码,从而改变该类的行为。
但是它的缺点也很明显:
Instrument 包是在整个虚拟机上挂了一个钩子程序,每次装入一个新类的时候,都必须执行一遍这段程序,即使这个类不需要改变。
直接改变字节码事实上类似于直接改写 class 文件,无论是调用 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
比较理想且流行的一种方法,即使用java.lang.ref.proxy。
首先,Proxy 编程是面向接口的。下面我们会看到,Proxy 并不负责实例化对象,和 Decorator 模式一样,要把 Account定义成一个接口,然后在 AccountImpl里实现 Account接口,接着实现一个 InvocationHandlerAccount方法被调用的时候,虚拟机都会实际调用这个 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 是面向接口的,所有使用 Proxy 的对象都必须定义一个接口, 而且用这些对象的代码也必须是对接口编程的:Proxy 生成的对象是接口一致的而不是对象一致的;
Proxy 毕竟是通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大 10 倍, 对于性能关键的应用,使用 proxy class 是需要精心考虑的,以避免反射成为整个应用的瓶颈;
ASM 能够通过改造既有类,直接生成需要的代码。
增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。
同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。
相比于其他流行的 Java 字节码操纵工具,ASM 更小更快。ASM 具有类似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分别有 350k 和 150k。同时,同样类转换的负载,如果 ASM 是 60% 的话,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。
小结
这里我们比较一下ASM和其他实现AOP的底层技术
AOP底层技术 | 功能 | 性能 | 面向接口编程 | 编程难度 |
---|---|---|---|---|
直接改写class文件 | 完全控制类 | 无明显性能代价 | 不要求 | 高,要求对class文件结构和Java字节码有深入的了解 |
JDK Instrument | 完全控制类 | 无论是否改写,每个类装入时都要执行 hook 程序 | 不要求 | 高,要求对 class 文件结构和 Java 字节码有深刻了解 |
JDK Proxy | 只能改写 method | 反射引入性能代价 | 要求 | 低 |
ASM | 几乎能完全控制类 | 无明显性能代价 | 不要求 | 中,能操纵需要改写部分的 Java 字节码 |
ps:摘自IBM Developer