AOP概述
软件的编程语言最终的目的就是用更自然更灵活的方式模拟世界,从原始机器语言到过程语言再到面向对象的语言,我们看到编程语言在一步步用更自然、更强大的方式描述软件。AOP是软件开发思想的一个飞跃,AOP的引入将有效弥补OOP的不足,OOP和AOP分别从纵向和横向对软件进行抽象,有效地消除重复性的代码,使代码以更优雅的更有效的方式进行逻辑表达。
AOP有三种植入切面的方法:其一是编译期织入,这要求使用特殊的Java编译器,AspectJ是其中的代表者;其二是类装载期织入,而这要求使用特殊的类装载器,AspectJ和AspectWerkz是其中的代表者;其三为动态代理织入,在运行期为目标类添加增强生成子类的方式,Spring AOP采用动态代理织入切面。
Spring AOP使用了两种代理机制,一种是基于JDK的动态代理,另一种是基于CGLib的动态代理,之所以需要两种代理机制,很大程度上是因为JDK本身只提供基于接口的代理,不支持类的代理。
基于JDK的代理和基于CGLib的代理是Spring AOP的核心实现技术,认识这两代理技术,有助于探究Spring AOP的实现机理。只要你愿意,你甚至可以抛开Spring,提供自己的AOP实现。
带有横切逻辑的实例
首先,我们来看一个无法通过OOP进行抽象的重复代码逻辑,它们就是AOP改造的主要对象。下面,我们通过一个业务方法性能监视的实例了解横切逻辑。业务方法性能监视,在每一个业务方法调用之前开始监视,业务方法结束后结束监视并给出性能报告:
代码清单 2 ForumService:包含性能监视横切代码
package com.baobaotao.proxy; public class ForumServiceImpl implements ForumService ...{
public void removeTopic(int topicId) ...{ //开始性能监视 PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeTopic"); System.out.println("模拟删除Topic记录:"+topicId); try ...{ Thread.currentThread().sleep(20); } catch (Exception e) ...{ throw new RuntimeException(e); } //结束监视、并给出性能报告信息 PerformanceMonitor.end(); }
public void removeForum(int forumId) ...{ //开始性能监视 PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeForum"); System.out.println("模拟删除Forum记录:"+forumId); try ...{ Thread.currentThread().sleep(40); } catch (Exception e) ...{ throw new RuntimeException(e); } //结束监视、并给出性能报告信息 PerformanceMonitor.end(); } }
|
代码清单 2中粗体表示的代码就是具有横切特征的代码,需要进行性能监视的每个业务方法的前后都需要添加类似的性能监视语句。
我们保证实例的完整性,我们提供了一个非常简单的性能监视实现类,如所示代码清单 3所示:
代码清单 3 PerformanceMonitor
package com.baobaotao.proxy;
public class PerformanceMonitor { //通过一个ThreadLocal保存线程相关的性能监视信息 private static ThreadLocal<MethodPerformace> performaceRecord = new ThreadLocal<MethodPerformace>(); public static void begin(String method) { System.out.println("begin monitor..."); MethodPerformace mp = new MethodPerformace(method); performaceRecord.set(mp); } public static void end() { System.out.println("end monitor..."); MethodPerformace mp = performaceRecord.get(); mp.printPerformace(); //打印出业务方法性能监视的信息 } }
|
PerformanceMonitor提供了两个方法,begin(String method)方法开始对某个业务类方法的监视,method为业务方法的签名,而end()方法结束对业务方法的监视,并给出性能监视的信息。由于每一个业务方法都必须单独记录性能监视数据,所以我们使用了ThreadLocal,ThreadLocal是削除非线程安全状态的不二法宝。ThreadLocal中的元素为方法性能记录对象MethodPerformace,它的代码如下所示:
代码清单 4 MethodPerformace
package com.baobaotao.proxy; public class MethodPerformace { private long begin; private long end; private String serviceMethod; public MethodPerformace(String serviceMethod){ this.serviceMethod = serviceMethod; this.begin = System.currentTimeMillis();//记录方法调用开始时的系统时间 } public void printPerformace(){ //以下两行程序得到方法调用后的系统时间,并计算出方法执行花费时间 end = System.currentTimeMillis(); long elapse = end - begin; //报告业务方法执行时间 System.out.println(serviceMethod+"花费"+elapse+"毫秒。"); } }
|
#p#
通过下面代码测试这个拥有方法性能监视能力的业务方法:
package com.baobaotao.proxy; public class TestForumService { public static void main(String[] args) { ForumService forumService = new ForumServiceImpl(); forumService .removeForum(10); forumService .removeTopic(1012); } }
|
我们得到以下的输出信息:
begin monitor... 模拟删除Forum记录:10 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeForum花费47毫秒。
begin monitor... 模拟删除Topic记录:1012 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeTopic花费16毫秒。
|
如实例所示,要对业务类进行性能监视,就必须在每个业务类方法的前后两处添加上重复性的开启性能监视和结束性能监视的代码。这些非业务逻辑的性能监视代码破坏了作为业务类ForumServiceImpl的纯粹性。下面,我们分别JDK动态代理和CGLib动态代理技术,将业务方法中开启和结束性能监视的这些横切代码从业务类中完成移除。
JDK动态代理
在JDK 1.3以后提供了动态代理的技术,允许开发者在运行期创建接口的代理实例。在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在我们终于发现动态代理是实现AOP的绝好底层技术。
JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,在并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。
而Proxy为InvocationHandler实现类动态创建一个符合某一接口的代理实例。这样讲一定很抽象,我们马上着手动用Proxy和InvocationHandler这两个魔法戒对上一节中的性能监视代码进行AOP式的改造。
首先,我们从业务类ForumServiceImpl 中删除性能监视的横切代码,使ForumServiceImpl只负责具体的业务逻辑,如所示:
代码清单 5 ForumServiceImpl:移除性能监视横切代码
package com.baobaotao.proxy; public class ForumServiceImpl implements ForumService { public void removeTopic(int topicId) { ① System.out.println("模拟删除Topic记录:"+topicId); try { Thread.currentThread().sleep(20); } catch (Exception e) { throw new RuntimeException(e); } ② } public void removeForum(int forumId) { ① System.out.println("模拟删除Forum记录:"+forumId); try { Thread.currentThread().sleep(40); } catch (Exception e) { throw new RuntimeException(e); } ② } }
|
在代码清单 5中的①和②处,原来的性能监视代码被移除了,我们只保留了真正的业务逻辑。
从业务类中移除的横切代码当然还得找到一个寄居之所,InvocationHandler就是横切代码的家园乐土,我们将性能监视的代码安置在PerformaceHandler中,如代码清单 6所示:
代码清单 6 PerformaceHandler
package com.baobaotao.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method;
public class PerformaceHandler implements InvocationHandler { private Object target; public PerformaceHandler(Object target){//①target为目标的业务类 this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { PerformanceMonitor.begin(target.getClass().getName()+"."+ method.getName()); Object obj = method.invoke(target, args);//②通过反射方法调用目标业务类的业务方法 PerformanceMonitor.end(); return obj; } }
|
粗体部分的代码为性能监视的横切代码,我们发现,横切代码只出现一次,而不是原来那样星洒各处。大家注意②处的method.invoke(),该语句通过反射的机制调用目标对象的方法,这样InvocationHandler的invoke(Object proxy, Method method, Object[] args)方法就将横切代码和目标业务类代码编织到一起了,所以我们可以将InvocationHandler看成是业务逻辑和横切逻辑的编织器。下面,我们对这段代码做进一步的说明。
#p#
首先,我们实现InvocationHandler接口,该接口定义了一个 invoke(Object proxy, Method method, Object[] args)的方法,proxy是代理实例,一般不会用到;method是代理实例上的方法,通过它可以发起对目标类的反射调用;args是通过代理类传入的方法参数,在反射调用时使用。
此外,我们在构造函数里通过target传入真实的目标对象,如①处所示,在接口方法invoke(Object proxy, Method method, Object[] args)里,将目标类实例传给method.invoke()方法,通过反射调用目标类方法,如②所示。
下面,我们通过Proxy结合PerformaceHandler创建ForumService接口的代理实例,如代码清单 7所示:
代码清单 7 TestForumService:创建代理实例
package com.baobaotao.proxy; import java.lang.reflect.Proxy; public class TestForumService { public static void main(String[] args) { ForumService target = new ForumServiceImpl();//①目标业务类 //② 将目标业务类和横切代码编织到一起 PerformaceHandler handler = new PerformaceHandler(target); //③为编织了目标业务类逻辑和性能监视横切逻辑的handler创建代理类 ForumService proxy = (ForumService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); //④ 操作代理实例 proxy.removeForum(10); proxy.removeTopic(1012); } }
|
上面的代码完成了业务类代码和横切代码编织和接口代理实例生成的工作,其中在②处,我们将ForumService实例编织为一个包含性能监视逻辑的PerformaceHandler实例,然后在③处,通过Proxy的静态方法newProxyInstance()为融合了业务类逻辑和性能监视逻辑的handler创建一个ForumService接口的代理实例,该方法的第一个入参为类加载器,第二个入参为创建的代理实例所要实现的一组接口,第三个参数是整合了业务逻辑和横切逻辑的编织器对象。
按照③处的设置方式,这个代理实例就实现了目标业务类的所有接口,也即ForumServiceImpl的ForumService接口。这样,我们就可以按照调用ForumService接口的实例相同的方式调用代理实例,如④所示。运行以上的代码,输出以下的信息:
begin monitor... 模拟删除Forum记录:10 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeForum花费47毫秒。
begin monitor... 模拟删除Topic记录:1012 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeTopic花费26毫秒。
|
我们发现,程序的运行效果和直接在业务类中编写性能监视逻辑的效果一致,但是在这里,原来分散的横切逻辑代码已经被我们抽取到PerformaceHandler中。当其它业务类(如UserService、SystemService等)的业务方法也需要使用性能监视时,我们只要按照以上的方式,分别为它们创建代理对象就可以了。下面,我们用时序图描述调用关系,进一步代理实例的本质,如图1所示:
图1:代理实例的时序图
我们在上图中特别使用虚线阴影的方式对通过代理器创建的ForumService实例进行凸显,该实例内部利用PerformaceHandler整合横切逻辑和业务逻辑。调用者调用代理对象的的removeForum()和removeTopic()方法时,上图的内部调用时序清晰地告诉了我们实际上所发生的一切。
CGLib动态代理
使用JDK创建代理有一个限制,即它只能为接口创建代理,这一点我们从Proxy的接口方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)就看得很清楚,第三个入参interfaces就是为代理实例指定的实现接口。虽然,面向接口的编程被很多很有影响力人(包括Rod Johnson)的推崇,但在实际开发中,开发者也遇到了很多困惑:难道对一个简单业务表的操作真的需要创建5个类(领域对象类、Dao接口,Dao实现类,Service接口和Service实现类)吗?对于这一问题,我们还是留待大家进一步讨论。现在的问题是:对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK的代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这个空缺。你可以从http://cglib.sourceforge.net/获取CGLib的类包,也可以直接从Spring的关联类库lib/cglib中获取类包。
#p#
CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并在拦截方法相应地织入横切逻辑。下面,我们采用CGLib技术,编写一个可以为任何类创建织入性能监视横切逻辑的代理对象的代理器,如代码清单 8所示:
代码清单 8 CglibProxy
package com.baobaotao.proxy; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor { private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz) { enhancer.setSuperclass(clazz); ① 设置需要创建子类的类 enhancer.setCallback(this); return enhancer.create(); ②通过字节码技术动态创建子类实例 } public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName()); Object result=proxy.invokeSuper(obj, args); ③ 通过代理类调用父类中的方法 PerformanceMonitor.end(); return result; } }
|
在上面代码中,你可以通过getProxy(Class clazz)为一个类创建动态代理对象,该代理对象是指定类clazz的子类。在这个代理对象中,我们织入性能监视的横切逻辑(粗体部分)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定义的Inerceptor接口的方法,obj表示父类的实例,method为父类方法的反射对象,args为方法的动态入参,而proxy为代理类实例。
下面,我们通过CglibProxy为ForumServiceImpl类创建代理对象,并测试代理对象的方法,如代码清单 9所示:
代码清单 9 TestForumService:测试Cglib创建的代理类
package com.baobaotao.proxy; import java.lang.reflect.Proxy; public class TestForumService { public static void main(String[] args) { CglibProxy proxy = new CglibProxy(); ForumServiceImpl forumService = //① 通过动态生成子类的方式创建代理对象 (ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class); forumService.removeForum(10); forumService.removeTopic(1023); } }
|
在①中,我们通过CglibProxy为ForumServiceImpl动态创建了一个织入性能监视逻辑的代理对象,并调用了代理对象的业务方法。运行上面的代码,输入以下的信息:
begin monitor... 模拟删除Forum记录:10 end monitor... com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花费47毫秒。 begin monitor... 模拟删除Topic记录:1023 end monitor... com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeTopic花费16毫秒。
|
观察以上的输出,除了发现两个业务方法中都织入了性能监控的逻辑外,我们还发现代理类的名字是com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,这个特殊的类就是CGLib为ForumServiceImpl所动态创建的子类。
小结
Spring AOP在底层就是利用JDK动态代理或CGLib动态代理技术为目标Bean织入横切逻辑。在这里,我们对以上两节动态创建代理对象做一个小结。
在PerformaceHandler和CglibProxy中,有三点值得注意的地方是:第一,目标类的所有方法都被添加了性能监视横切的代码,而有时,这并不是我们所期望的,我们可能只希望对业务类中的某些方法织入横切代码;第二,我们手工指定了织入横切代码的织入点,即在目标类业务方法的开始和结束前调用;第三,我们手工编写横切代码。以上三个问题,在AOP中占用重要的地位,因为Spring AOP的主要工作就是围绕以上三点展开:Spring AOP通过Pointcut(切点)指定在哪些类的哪些方法上施加横切逻辑,通过Advice(增强)描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等),此外,Spring还通过Advisor(切面)组合Pointcut和Advice。有了Advisor的信息,Spring就可以利用JDK或CGLib的动态代理技术为目标Bean创建织入切面的代理对象了。
JDK动态代理所创建的代理对象,在JDK 1.3下,性能强差人意。虽然在高版本的JDK中,动态代理对象的性能得到了很大的提高,但是有研究表明,CGLib所创建的动态代理对象的性能依旧比JDK的所创建的代理对象的性能高不少(大概10倍)。而CGLib在创建代理对象时性能却比JDK动态代理慢很多(大概8倍),所以对于singleton的代理对象或者具有实例池的代理,因为不需要频繁创建代理对象,所以比较适合用CGLib动态代理技术,反之适合用JDK动态代理技术。此外,由于CGLib采用生成子类的技术创建代理对象,所以不能对目标类中的final方法进行代理。