但是如果您希望改变应用程序的常规行为呢?例如说,您希望重写一个方法?这样的话,您就需要使用更积极的around形式的通知。
第一部分的简单例子应用程序包括IbusinessLogic接口、BusinessLogic类和MainApplication类,如下所示:
- public interface IBusinessLogic
- {
- public void foo();
- }
- public class BusinessLogic
- implements IBusinessLogic
- {
- public void foo()
- {
- System.out.println(
- "Inside BusinessLogic.foo()");
- }
- }
- import org.springframework.context.ApplicationContext;
- import org.springframework.context.support.FileSystemXmlApplicationContext;
- public class MainApplication
- {
- public static void main(String [] args)
- {
- // Read the configuration file
- ApplicationContext ctx =
- new FileSystemXmlApplicationContext(
- "springconfig.xml");
- //Instantiate an object
- IBusinessLogic testObject =
- (IBusinessLogic) ctx.getBean(
- "businesslogicbean");
- // Execute the public
- // method of the bean
- testObject.foo();
- }
- }
要对一个BusinessLogic类的实例彻底重写对foo()方法的调用,需要创建around通知,如下面的AroundAdvice类所示:
- import org.aopalliance.intercept.MethodInvocation;
- import org.aopalliance.intercept.MethodInterceptor;
- public class AroundAdvice
- implements MethodInterceptor
- {
- public Object invoke(
- MethodInvocation invocation)
- throws Throwable
- {
- System.out.println(
- "Hello world! (by " +
- this.getClass().getName() +
- ")");
- return null;
- }
- }
要在spring中用作around通知,AroundAdvice类必须实现MethodInterceptor接口和它的invoke(..)方法。每当截获到方法的重写,invoke(..)方法就会被调用。最后一步是改变包含在应用程序的springconfig.xml文件中的Spring运行时配置,以便可以对应用程序应用AroundAdvice。
- <?xml version="1.0" encoding="UTF-8"?>
- <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
- "http://www.springframework.org/dtd/spring-beans.dtd">
- <beans>
- <!-- Bean configuration -->
- <bean id="businesslogicbean"
- class="org.springframework.aop.framework.ProxyFactoryBean">
- <property name="proxyInterfaces">
- <value>IBusinessLogic</value>
- </property>
- <property name="target">
- <ref local="beanTarget"/>
- </property>
- <property name="interceptorNames">
- <list>
- <value>theAroundAdvisor</value>
- </list>
- </property>
- </bean>
- <!-- Bean Classes -->
- <bean id="beanTarget"
- class="BusinessLogic"/>
- <!-- Advisor pointcut definition for around advice -->
- <bean id="theAroundAdvisor"
- class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
- <property name="advice">
- <ref local="theAroundAdvice"/>
- </property>
- <property name="pattern">
- <value>.*</value>
- </property>
- </bean>
- <!-- Advice classes -->
- <bean id="theAroundAdvice"
- class="AroundAdvice"/>
- </beans>
根据该springconfig.xml配置文件,theAroundAdvisor截获所有对BusinessLogic类的方法的调用。接下来,theAroundAdvisor被关联到theAroundAdvice,表明当截获一个方法时,就应该使用在AroundAdvice类中指定的通知。既然已经指定了around通知的正确配置,下一次执行MainApplication类时,BusinessLogic bean的foo()方法就会被截获并重写,如图3所示:
图3. 使用around通知重写对BusinessLogic类中的foo()方法的调用
前面的例子显示,BusinessLogic类中的foo()方法可以通过AroundAdvice类中的invoke(..)方法彻底重写。原来的foo()方法完全不能被invoke(..)方法调用。如果希望从around通知内调用foo()方法,可以使用proceed()方法,可从invoke(..)方法的MethodInvocation参数中得到它。
- public class AroundAdvice
- implements MethodInterceptor
- {
- public Object invoke(
- MethodInvocation invocation)
- throws Throwable
- {
- System.out.println(
- "Hello world! (by " +
- this.getClass().getName() +
- ")");
- invocation.proceed();
- System.out.println("Goodbye! (by " +
- this.getClass().getName() +
- ")");
- return null;
- }
- }
图4显示了对proceed()的调用如何影响操作的顺序(与图3所示的初始around通知执行相比较)。
图4. 从around通知内使用proceed()调用原来的方法
当调用proceed()时,实际是在指示被截获的方法(在本例中是foo()方法)利用包含在MethodInvocation对象中的信息运行。您可以通过调用MethodInvocation类中的其他方法来改变该信息。
您可能希望更改包含在MethodInvocation类中的信息,以便在使用proceed()调用被截获的方法之前对被截获方法的参数设置新值。
通过对MethodInvocation对象调用getArguments()方法,然后在返回的数组中设置其中的一个参数对象,最初传递给被截获的方法的参数可以被更改。
如果IbusinessClass和BusinessLogic类的foo()方法被更改为使用整型参数,那么就可以将传递给被截获的调用的值由在AroundAdvice的notify(..)方法中传递改为在foo(int)中传递。
- public class AroundAdvice
- implements MethodInterceptor
- {
- public Object invoke(
- MethodInvocation invocation)
- throws Throwable
- {
- System.out.println(
- "Hello world! (by " +
- this.getClass().getName() +
- ")");
- invocation.getArguments()[0] = new Integer(20);
- invocation.proceed();
- System.out.println(
- "Goodbye! (by " +
- this.getClass().getName() +
- ")");
- return null;
- }
- }
在本例中,被截获的方法的第一个形参被假设为int。实参本身是作为对象传递的,所以通过将其包装在Integer类实例中的方法,基本的int类型的形参被改为对应数组中的新值。如果您将该参数设置为一个非Integer对象的值,那么在运行时就会抛出IllegalArgumentException异常。
您还将注意到,invoke(..)方法必须包含一个return语句,因为该方法需要返回值。但是,被重写的foo()方法并不返回对象,所以invoke(..)方法可以以返回null结束。如果在foo()方法不需要的情况下,您仍然返回了一个对象,那么该对象将被忽略。
如果foo()方法确实需要返回值,那么需要返回一个与foo()方法的初始返回类型在同一个类或其子类中的对象。如果foo()方法返回一个简单类型,例如,一个integer,那么您需要返回一个Integer类的对象,当方法被重写时,该对象会自动由AOP代理拆箱,如图5所示:
图5. around通知的装箱和自动拆箱
图字:
Object invoke:对象调用
The integer return value is boxed in a Integer object in the AroundAdvice and then unboxed by the AOP Proxy:整型返回值被装箱在AroundAdvic通知的一个Integer对象中,然后由AOP代理拆箱。
面向方面编程还是一个比较新的领域,尤其是与衍生出它的面向对象编程相比。设计模式通常被认为是常见问题的通用解决方案,因为面向方面发展的时间还不长,所以已发现的面向方面设计模式比较少。
此处要介绍的是一种正在浮现的模式,即Cuckoo's Egg设计模式。该模式还有其他的叫法,它在面向对象领域的对等体包括模仿对象(Mock Object)和模仿测试(Mock Testing),甚至代理模式也与它有一些类似之处。
Cuckoo's Egg面向方面设计模式可以被定义为应用程序上下文中功能部件的透明和模块化的置换。就像杜鹃偷偷地把自己的蛋放在另一种鸟的巢中一样,Cuckoo's Egg设计模式用一个替代功能部件实现置换现有的功能部件,而使造成的干扰尽可能少。
这种置换的实现方式可以是静态的、动态的、部分的、完全的,针对一个对象的多个部分,或针对多个组件。使用面向方面的方法可以透明地实现功能部件的置换,而无需对应用程序的其余部分进行更改。要置换应用程序中现有功能部件的替代功能部件就是“杜鹃的蛋”。图6显示了Cuckoo's Egg设计模式中的主要组成元素。
图6. Cuckoo's Egg设计模式中的主要组成元素
图字:
Application:应用程序
Component:组件
Replacement Feature:替代功能部件
Component 1 and 2 together encompass a distinct feature of the software:组件1和2共同包含了软件的一个独立的功能部件
The Cuckoo's Egg pattern transparently replaces an existing feature of the software:Cuckoo's Egg模式透明地置换了软件现有的功能部件
Before the pattern is applied:应用该模式前
After the pattern is applied:应用该模式后
Cuckoo's Egg设计模式依赖于around通知的概念。您需要借助于积极的和侵入性的around通知来截获并有效置换应用程序中现有的功能部件。
有关Cuckoo's Egg设计模式的更多信息,以及AspectJ中的一个可选实现,请参见《AspectJ Cookbook》(O'Reilly,2004年12月出版)。
要使用Spring AOP实现Cuckoo's Egg设计模式,需要声明一个around通知来截获所有对要置换的功能部件的调用。与hot-swappable target sources(Spring AOP的一个功能部件,将在本系列的另一篇文章中介绍)不同,around通知的显式使用使得Cuckoo's Egg实现可以有效地跨越对象边界(因此也可以跨越bean边界)进行整个功能部件的置换,如图7所示。
图7. 一个跨越bean边界的组件
图字:
A feature crosses the boundaries of BusinessLogic and BusinessLogic2 by depending on behavior supplied separately by the two beans:一个功能部件通过依赖于由BusinessLogic和BusinessLogic2各自提供的行为而跨越了这两个bean的边界
下面的代码显示了一个具有两个bean的简单应用程序,其中有一个功能部件跨越了该应用程序的多个方面。要置换的功能部件可以被视为包含IBusinessLogic bean中的foo()方法和IBusinessLogic2 bean中的bar()方法。IBusinessLogic2 bean中的baz()方法不是 该功能部件的一部分,所以不进行置换。
- public interface IBusinessLogic
- {
- public void foo();
- }
- public interface IBusinessLogic2
- {
- public void bar();
- public void baz();
- }
此处,ReplacementFeature类扮演了“杜鹃的蛋”的角色,它提供了将被透明地引入应用程序的替代实现。ReplacementFeature类实现了所有在该类引入时要被置换的方法。
- public class ReplacementFeature
- {
- public void foo()
- {
- System.out.println(
- "Inside ReplacementFeature.foo()");
- }
- public void bar()
- {
- System.out.println(
- "Inside ReplacementFeature.bar()");
- }
- }
现在需要声明一个around通知来截获对跨越bean的功能部件的方法调用。CuckoosEgg类提供了某种around通知来检查被截获的方法,并将适当的方法调用传递给ReplacementFeature类的实例。
- public class CuckoosEgg implements MethodInterceptor
- {
- public ReplacementFeature replacementFeature =
- new ReplacementFeature();
- public Object invoke(MethodInvocation invocation)
- throws Throwable
- {
- if (invocation.getMethod().getName().equals("foo"))
- {
- replacementFeature.foo();
- }
- else
- {
- replacementFeature.bar();
- }
- return null;
- }
- }
因为与Spring框架关系密切,Cuckoo's Egg设计的详细信息被放在springconfig.xml配置文件中。对springconfig.xml文件的更改将确保所有对IbusinessLogic和IBusinessLogic2 bean的foo()方法和bar()方法的调用都将被截获,并传递给CuckoosEgg类的around通知。
- ...
- <!--CONFIG-->
- <bean id="businesslogicbean"
- class="org.springframework.aop.framework.ProxyFactoryBean">
- <property name="proxyInterfaces">
- <value>IBusinessLogic</value>
- </property>
- <property name="target">
- <ref local="beanTarget"/>
- </property>
- <property name="interceptorNames">
- <list>
- <value>theCuckoosEggAdvisor</value>
- </list>
- </property>
- </bean>
- <bean id="businesslogicbean2"
- class="org.springframework.aop.framework.ProxyFactoryBean">
- <property name="proxyInterfaces">
- <value>IBusinessLogic2</value>
- </property>
- <property name="target">
- <ref local="beanTarget2"/>
- </property>
- <property name="interceptorNames">
- <list>
- <value>theCuckoosEgg2Advisor</value>
- </list>
- </property>
- </bean>
- <!--CLASS-->
- <bean id="beanTarget" class="BusinessLogic"/>
- <bean id="beanTarget2" class="BusinessLogic2"/>
- <!--ADVISOR-->
- <bean id="theCuckoosEggAdvisor"
- class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
- <property name="advice">
- <ref local="theReplacementFeaturePart1Advice"/>
- </property>
- <property name="pattern">
- <value>IBusinessLogic.*</value>
- </property>
- </bean>
- <bean id="theCuckoosEgg2Advisor"
- class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
- <property name="advice">
- <ref local="theReplacementFeaturePart2Advice"/>
- </property>
- <property name="pattern">
- <value>IBusinessLogic2.bar*</value>
- </property>
- </bean>
- <!--ADVICE-->
- <bean id="theReplacementFeaturePart1Advice" class="CuckoosEgg"/>
- <bean id="theReplacementFeaturePart2Advice" class="CuckoosEgg"/>
- ...
当使用修改后的springconfig.xml文件运行例子应用程序时,要替换的、被指定为功能部件的一部分的方法调用完全被截获并传递给ReplacementFeature类。
通常,即使在同一个实现环境中,我们也可以用不同的方法来实现同一种设计模式。实现上例的另一种方法是实现两个独立的通知。
最后需要注意的是,使用Cuckoo's Egg设计模式置换的功能部件,不管它是跨越bean的还是在一个类中,它的生命周期与它所置换的功能部件的目标生命周期匹配。在上例中这没什么问题,因为只有一个功能部件实例被置换了,而且唯一的Cuckoo's Egg通知只维护一个替代功能部件。
这个例子非常简单,而在实践中,您很可能必须处理大量需要用各自的Cuckoo's Egg实例置换的功能部件实例。在这种情况下,单个的方面实例需要被关联到单个的要置换的功能部件实例。本系列的下一篇文章将会考虑方面生命周期的用法,届时将解决这个问题。
结束语
本文介绍了如何在Spring框架内谨慎使用around形式的通知。around形式的通知常用于实现Cuckoo's Egg设计模式时,所以我们引入了一个例子来说明如何使用Spring AOP实现这种面向方面设计模式。
在本系列的第三部分中,您将看到如何使用Spring框架中其他的AOP基本概念。这些概念包括:控制方面生命周期、使用基于introduction通知的积极方面改变应用程序的静态结构,以及使用control flow切入点实现对方面编织的更细微的控制。