15.2.3 AOP实现技术

    目前Java的AOP实现技术主要有以下几种,了解这些技术对读者使用和深入了解这些框架是非常有帮助的,笔者在这里将为大家一一介绍。

    1.J2SE动态代理

    我们在代理模式一章为大家介绍了J2SE动态代理技术,它可以动态代理所有指定的接口方法,这为AOP的实现提供了一个契机:动态代理把所有代理接口的方法转发给InvocationHandler的invoke(……)方法统一处理,我们在运行时就可以很轻松地根据切入点为这些方法织入增强,由于接口一致,客户对象在使用时并不会察觉到不同之处。

    我们看看如何使用动态代理解决这个计时问题。由于动态代理只能代理接口,所以我们还需要提供一个IHappyPeople接口,如下所示。

    figure_0233_0243

    HappyPeopleHandler类实现了InvocationHandler接口,在invoke(Object proxy, Method method, Object[]args)方法里,我们实现此环绕增强的逻辑,代码大致如下所示。

    figure_0233_0244

    figure_0234_0245

    figure_0235_0246

    figure_0236_0247

    其中isMethodQualified(Method method)方法判断该代理方法是否需要增强,它封装了切入点的逻辑。

    在调用代理方法之前,如果满足切入点条件,则织入开始计时的逻辑,即:“stopwatch.reset():”。

    同样,执行完代理方法之后,如果满足条件则织入这句用来打印的时间:

    stopwatch. info("Method"+method.getName()+"()used")

    这样,一个简单的AOP环绕增强的例子完成了。

    对于切入点逻辑,可以使用更为通用的方式实现,譬如可以根据模式字符串来判断是否需要增强,这样,上例中切入点的逻辑可以写为类似于这样的模式匹配字符串:“.subscribeTicket()||.travel()||**.celebrate()”。

    接下来我们写出测试代码,如下所示。

    figure_0236_0248

    figure_0237_0249

    以下是某一次的执行结果。

    figure_0237_0250

    这样,我们把记录时间的逻辑封装在了HappyPeopleHandler类里,再也看不到重复的代码。

    注意:细心的读者可能发现了,我们在测试代码中未直接调用HappyPeople的celebrateSpringFestival()方法,而是依次调用了它的subscribeTicket()、travel()和celebrate()这三个方法。如果直接调用celebrateSpringFestival()方法,你就会发现并未执行计时逻辑,这是什么原因呢?

    原来,动态代理把方法请求交给了HappyPeopleHandler的invoke(Object proxy, Method method, Object[]args)方法,它会调用目标对象的celebrateSpringFestival()方法,即HappyPeople对象的celebrateSpringFestival()方法,目标对象调用自己的这三个未织入增强的方法,而不是调用代理对象的那三个可以织入增强的方法,所以没有打印出执行时间。

    如何解决这个问题呢?其实方法很多,我们可以把庆祝团圆封装成一个独立的Celebration类,它的方法为:celebrateSpringFestival(IHappyPeople ppl),代码大致如下所示。

    figure_0239_0252

    这样,客户对象传入织入增强的代理对象,执行Celebration对象的celebrate SpringFestival(IHappyPeople ppl)方法即可。

    其实聪明的读者可能想到了在HappyPeople使用如下方法。

    figure_0239_0253

    这种方法虽然可以解决代理对象未彻底代理所有方法引起的问题,但是从模型上来考虑,它是欠妥的。HappyPeople类的职责会变得不明确——为什么不是自己而是别人来庆祝团圆?并且使用static方法会丧失面向对象所带来的继承和多态等特性,不易扩展。

    一个优秀的软件开发人员,不仅要熟悉各种模式,还需要为问题抽象出精炼的模型,关于如何提炼模型,请参见下章。

    因为Java语言自动支持动态代理技术,所以不需要依赖于任何第三方软件和库,移植性和兼容性得到了保证。但是,一方面动态代理只能局限于接口的代理,不能对类进行代理;另一方面,通过上述示例,我们看到由于Java动态代理的实现机制问题——把方法转发给了目标对象,导致逻辑织入地不够彻底。

    动态代理作为AOP的一种实现技术,被广泛应用到了诸多框架之中,例如Nanning、Spring、Pico Container等。而这些成熟的AOP框架往往结合一些其他AOP技术(如动态字节码、拦截器框架等),以实现更强大的AOP框架。

    2.动态字节码

    由于Java语言的开放性,涌现出了很多动态字节码工具,像Apache的BCEL。Jboss的Javassist。ObjectWeb的ASM和开源工具CGLib等,它们可以在运行时分析、生成和改变Java Class文件,在运行时也可以非常方便地织入增强,是AOP实现的另一条途径。这方面比较流行的工具是CGLib,它曾被Hibernate早期版本成功地应用于延迟加载等方面,我们这里就以其为例作介绍。

    CGLib的net.sf.cglib.proxy.MethodInterceptor和java.lang.reflect.Invocation Handler一样,都能统一控制被代理的方法,使用二者的区别不大,代码如下所示。

    figure_0241_0255

    figure_0242_0256

    figure_0243_0257

    可以看到,除了intercept(Object o, Method method, Object[]objects, MethodProxy methodProxy)的方法签名不同之外,代码并未和Java动态代理的InvocationHandler实现类有很大区别。

    CGLib不仅可以代理接口,还可以代理类,所以这里我们不再创建接口。以下代码显示如何使用CGLib生成代理类的对象。

    figure_0243_0258

    我们的测试代码如下所示。

    figure_0243_0259

    figure_0244_0260

    这里给出了测试的某一次执行结果,如下所示。

    figure_0244_0261

    figure_0245_0262

    细心的读者可能早就发现,我们这次直接调用HappyPeople类的celebrate SpringFestival()方法而完成了执行时间的计算,这是由于CGLib生成代理类的方式和J2SE动态代理生成代理类的方式不同引起的。

    J2SE动态代理生成代理类继承于java.lang.reflect.Proxy类,所有方法指向了InvocationHandler实现对象的invoke(……)方法,而invoke(……)方法把请求再次转发给目标对象处理;

    CGLib生成的代理类继承于被代理类/目标类,这样,上述执行代理对象的celebrateSpringFestival()方法时,而不会把请求直接转发给目标对象,而是执行自己被织入增强的方法,也正是由于此缘故——使用继承的方式创建代理类,CGLib不能代理任何final的方法和类。

    通过介绍CGLib,旨在希望大家对Java字节码工具在AOP方面的实现上有个大体的认识,由于这种工具很多,限于篇幅,不再赘述。关于CGLib的详细使用,有兴趣的读者可以登录:http://cglib.sourceforge.net/

    3.拦截器(Interceptor)框架

    拦截器(Interceptor)一般在Command框架里都有相应实现,它能围绕被执行的业务对象织入增强。我们可以把切面都封装在拦截器的具体实现类里。当然拦截器框架是使用OOP语言设计的一个框架,其中往往结合使用了Java动态代理、字节码工具和Java反射(Reflection)等技术。

    拦截器框架的一般结构如图15-1所示。

    figure_0246_0263

    图15-1

    客户对象直接使用的是代理对象BusinessObjProxy,它持有控制执行逻辑的Invocation对象。

    Invocation持有Interceptor链和真正被代理的BusinessObj对象,和前述invoke(……)方法的功能一样,负责织入增强,即依次调用拦截器进行拦截,最终执行BusinessObj对象的方法。

    Interceptor实现类便是我们的切面。

    BusinessObj是目标对象,即要被织入增强的对象。

    Invocation负责BusinessObjProxy的执行,在执行过程中它负责Interceptor链的每个拦截器方法被依次执行,以及被代理对象BusinessObj的执行。为了便于描述,我们给出拦截器的序列图,如图15-2所示。

    figure_0247_0264

    图15-2

    Client调用BusinessObjProxy对象的invoke()方法,BusinessObjProxy把请求转给Invocation对象,Invocation执行完拦截器链,接着执行BusinessObj的方法,最后执行拦截器链未执行完的堆栈逻辑,并返回执行结果。

    我们在这里实现一个非常简单的拦截器框架,来说明拦截器框架的工作原理。首先实现定义一个Invocation接口,代码如下。

    figure_0248_0265

    接下来给出一个简单的Invocation实现,经上述介绍,我们知道,拦截器框架织入逻辑会在此类中实现,如下所示。

    figure_0248_0266

    figure_0249_0267

    figure_0250_0268

    figure_0251_0269

    我们的织入逻辑在invoke()方法里:首先判断拦截器链是否未执行完毕,即:if(interceptors.hasNext()),如果没有,则取出下一个拦截器继续执行;如果拦截器链执行完毕,这才去执行真正被代理对象的方法,即:result=invokeBusinessObjectMethod()。

    invokeBusinessObjectMethod()方法会调用目标方法,我们这里的实现非常简单、使用反射得到配置的方法并执行。

    ProxyFactory工厂类用于创建BusinessObjectProxy对象,代码如下。

    figure_0251_0270

    figure_0252_0271

    Interceptor接口的代码如下所示。

    figure_0252_0272

    我们这里实现记录时间的拦截器StopWatchInterceptor,代码如下。

    figure_0252_0273

    figure_0253_0274

    我们在Object result=invocation.invoke()之前加入这句stopwatch.reset()开始计时,Object result=invocation.invoke()这句非常重要,保证了Invocation能够继续执行拦截器链上其他还未执行的拦截器以及目标对象的被拦截方法。

    最后,由于方法的堆栈调用,Invocation仍然要返回该拦截器,执行连接点invocation.invoke()之后的逻辑,即打印出所消耗的时间,即:stopwatch.info("……")。

    我们这里做了一个非常简单的配置类Config,用于描述对象的哪个方法需要拦截,当然你可以撰写更为复杂的配置支持你的框架,代码如下所示。

    figure_0253_0275

    在使用之前,和其他框架一样,我们需要建立上下文环境,我这里写了一个简单的NaiveInterceptorFrameWork类初始化环境上下文,如下所示。

    figure_0254_0276

    figure_0255_0277

    我们为PassengerByAir对象的三个方法[subscribeTicket()、travel()和celebrate()]配置了StopWatchInterceptor拦截器,测试代码如下所示。

    figure_0255_0278

    figure_0256_0279

    执行结果如下所示。

    figure_0256_0280

    优点:

    我们不需要在编译和加载类时做任何额外的操作,方便编译和发布。从上述实现我们知道,可以对任何对象进行拦截。

    从上述例子知道,我们可以对同一类型的不同对象织入不同的逻辑,只要为它们配置不同的拦截器即可。

    拦截器的代码不会侵入目标的类和方法。

    缺点:

    框架的不同,拦截器的能力强弱差别比较大。

    框架的一次执行只能拦截一个目标方法,如果要统计三个方法的执行时间,我们要配置三个拦截器链。

    拦截器在很多框架中均有实现,最常见的有Jboss的拦截器、xwork框架[1]的拦截器和Spring AOP的拦截器等,这些框架往往结合其他AOP技术,例如J2SE动态代理和动态字节码。

    4.源代码生成

    在EJB早期实现里,我们经常使用这种方式生成织入增强的Java源代码,这种方式随着Java动态代理技术和字节码工具的出现已经开始逐渐退出潮流。

    5.在编译时织入二进制代码

    我们可以使用特定的编译器在编译时织入增强,这种方式的织入,运行性能总比运行时织入要优越一些[2]。它的运行速度可以媲美Java源代码生成,这二者都是静态织入,目标对象被织入的功能在编译时就决定了。

    AspectJ和AspectWerkz都支持这种策略,Cobertura是一款统计测试代码覆盖率的软件,它就是在编译时把统计覆盖率的逻辑织入已编译好的Java二进制代码里,然后运行这些二进制文件,完成了覆盖率的统计。

    6.定制类加载器(Class Loader)

    我们还有一种织入方式,那就是在类加载器加载类时织入增强,由于Java类加载器的可扩展性,我们可以定制自己的类加载器,让其在加载类时织入增强。AspectWerkz(称之为在线织入,Online Weaving)和AspectJ都支持这种方式。

    但是这种方式可能在某些J2EE服务器上并不能被使用,因为这些服务器一般有一套自制的完整类加载结构体系,这种方式可能导致不能定制我们自己的类加载器,否则会引起冲突。

    [1]xwork框架是一款非常出色的command框架软件,著名的Struts2和Webwork框架就是使用其实现的,它的拦截器实现了对command/action的动态拦截。

    [2]其实运行时织入并没有想象中那么慢,现在由于JVM性能的提升,对动态代理等做了很大优化,那些字节码工具在运行时表现也越来越出色。