時間:2005-12-23
作者:
Russell Miles瀏覽次數(shù):
3616
本文關(guān)鍵字:
Spring,?
Java,?
AOP,?
crosscutting,?
面向方面編程,?
橫切,?
Cuckoo's Egg模式 在本系列的第一部分,我介紹了如何實(shí)現(xiàn)面向方面領(lǐng)域的“HelloWorld”:跟蹤和記錄方面。利用Spring框架所提供的面向方面編程(Aspect-Oriented Programming,AOP)功能,您看到了如何使用before-、after-和基于異常的通知,以及如何使用基于正則表達(dá)式的簡單切入點(diǎn)。跟蹤和記錄方面提供了非常不錯的上手例子,而本文將進(jìn)一步介紹一種新的通知形式:around通知。
比起第一部分中介紹的那些通知類型,around形式的通知是一種更具侵入性也更強(qiáng)大的面向?qū)ο蟾拍睢1疚膶⒚枋鯽round通知的每個特性,以便您可以在自己的Spring AOP應(yīng)用程序中正確地使用它。在本文最后,我將向您展示如何使用around通知來截獲和改變應(yīng)用程序中各個特性相互作用的方式,以便實(shí)現(xiàn)Cuckoo's Egg(杜鵑的蛋)面向方面設(shè)計模式。
概述Spring AOP、IoC和代理
在第一部分,我們快速瀏覽了Spring的一些AOP特性,而沒有闡明Spring如何實(shí)現(xiàn)AOP的細(xì)節(jié)。要理解Spring框架如何運(yùn)轉(zhuǎn),尤其是它如何實(shí)現(xiàn)其AOP功能,首先您要明白,Spring是一個依賴于控制反轉(zhuǎn)(Inversion of Control,IoC)設(shè)計模式的輕量級框架。
注意:本文的目的不是要深入介紹IoC模式,介紹IoC只是為了使您明白該設(shè)計模式是如何影響Spring AOP實(shí)現(xiàn)的。有關(guān)IoC模式的更詳細(xì)的介紹請參見本文末尾的參考資料。
IoC設(shè)計模式的出現(xiàn)已經(jīng)有一段時間了。一個最明顯的例子就是J2EE架構(gòu)本身。隨著企業(yè)開發(fā)尤其是J2EE平臺的出現(xiàn),應(yīng)用程序開始依賴于由外部容器所提供的一些特性,比如bean創(chuàng)建、持久性、消息傳遞、會話以及事務(wù)管理。
IoC引入了一個新概念:由組件構(gòu)成的框架,它與J2EE容器有許多類似之處。IoC框架分離了組件所依賴的功能,并且,根據(jù)Sam Newman文章中的說法,提供了“連接組件的‘膠水’”。
對組件所依賴特性的控制 被反轉(zhuǎn) 了,這樣外部框架就可以盡可能透明地提供這些特性了。IoC模式真正意識到了從傳統(tǒng)的由依賴于功能的組件來負(fù)責(zé)這些功能,到由獨(dú)立的框架來配置和提供這些功能的方式轉(zhuǎn)變。
圖1顯示了一些構(gòu)成IoC模式的不同組件角色的例子。

圖1. 沒有對BusinessLogic bean應(yīng)用方面時的順序圖.
圖字:
Component:組件
Provides Facilities:提供功能
Relies on and conforms to:依賴于并服從
Manages the services the framework can then use to provide facilities:管理框架隨后可以用來提供功能的服務(wù)
Service:服務(wù)
Your Component:您的組件
IoC Framework:IoC框架
External services:外部服務(wù)
IoC模式使用3種不同的方法來解除組件與服務(wù)控制的耦合:類型1、類型2和類型3。
- 類型1:接口注入
這是大部分J2EE實(shí)現(xiàn)所使用的方法。組件顯式地服從于一組接口,帶有關(guān)聯(lián)的配置元數(shù)據(jù),以便允許框架對它們進(jìn)行正確的管理。
- 類型2:Setter注入
外部元數(shù)據(jù)被用來配置組件相互作用的方式。在第一部分中,我們就是使用這種IoC方法利用springconfig.xml文件來配置Spring組件的。
- 類型3:構(gòu)造函數(shù)注入
組件(包括構(gòu)造組件時要用的參數(shù))注冊到框架,而框架提供組件的實(shí)例以及所有要應(yīng)用的指定功能。
IoC在組件開發(fā)和企業(yè)開發(fā)中越來越受歡迎。IoC的實(shí)際例子包括傳統(tǒng)的J2EE解決方案,比如:JBoss、Apache基金會的Avalon項(xiàng)目以及本文的Spring框架。實(shí)際上,Spring框架構(gòu)建于IoC模式的基礎(chǔ)上是為了幫助將它的輕量級功能注入到它的相關(guān)應(yīng)用程序的組件中。
那么IoC對于Spring AOP有何意義呢?Spring的IoC特性是使用IoC springconfig.xml配置文件對應(yīng)用程序應(yīng)用方面的推動因素之一。springconfig.xml配置文件通知Spring框架運(yùn)行時有關(guān)應(yīng)用程序的組件要被注入的功能類型的信息,所以自然輕量級的AOP功能就以同樣的方式應(yīng)用了。然后Spring使用代理模式圍繞現(xiàn)有的類和bean實(shí)現(xiàn)指定的AOP功能。
圖2顯示了Spring及其IoC框架如何使用代理對象提供AOP功能(根據(jù)springconfig.xml文件中的IoC配置。)

圖2. springconfig.xml配置文件改變了Spring框架IoC,以便隨后向第一部分中的一個順序圖提供AOP代理(單擊圖像查看大圖)
在本系列下面的部分,您將不斷看到現(xiàn)在包含在順序圖中的代理對象。這只是為了說明對于Spring AOP來說沒有“魔法”,實(shí)際上只有一個面向?qū)ο笤O(shè)計模式的良好例子。
回到AOP:使用around通知的積極方面
在第一部分,您看到了如何使用Spring AOP來實(shí)現(xiàn)跟蹤和記錄方面。跟蹤和記錄都是“消極”方面,因?yàn)樗鼈兊某霈F(xiàn)并不會對應(yīng)用程序的其他行為產(chǎn)生影響。它們都使用了消極的before和after形式的通知。
但是如果您希望改變應(yīng)用程序的常規(guī)行為呢?例如說,您希望重寫一個方法?這樣的話,您就需要使用更積極的around形式的通知。
第一部分的簡單例子應(yīng)用程序包括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類的實(shí)例徹底重寫對foo()方法的調(diào)用,需要創(chuàng)建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類必須實(shí)現(xiàn)MethodInterceptor接口和它的invoke(..)方法。每當(dāng)截獲到方法的重寫,invoke(..)方法就會被調(diào)用。最后一步是改變包含在應(yīng)用程序的springconfig.xml文件中的Spring運(yùn)行時配置,以便可以對應(yīng)用程序應(yīng)用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>
根據(jù)該springconfig.xml配置文件,theAroundAdvisor截獲所有對BusinessLogic類的方法的調(diào)用。接下來,theAroundAdvisor被關(guān)聯(lián)到theAroundAdvice,表明當(dāng)截獲一個方法時,就應(yīng)該使用在AroundAdvice類中指定的通知。既然已經(jīng)指定了around通知的正確配置,下一次執(zhí)行MainApplication類時,BusinessLogic bean的foo()方法就會被截獲并重寫,如圖3所示:

圖3. 使用around通知重寫對BusinessLogic類中的foo()方法的調(diào)用
前面的例子顯示,BusinessLogic類中的foo()方法可以通過AroundAdvice類中的invoke(..)方法徹底重寫。原來的foo()方法完全不能被invoke(..)方法調(diào)用。如果希望從around通知內(nèi)調(diào)用foo()方法,可以使用proceed()方法,可從invoke(..)方法的MethodInvocation參數(shù)中得到它。
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()的調(diào)用如何影響操作的順序(與圖3所示的初始around通知執(zhí)行相比較)。

圖4. 從around通知內(nèi)使用proceed()調(diào)用原來的方法
當(dāng)調(diào)用proceed()時,實(shí)際是在指示被截獲的方法(在本例中是foo()方法)利用包含在MethodInvocation對象中的信息運(yùn)行。您可以通過調(diào)用MethodInvocation類中的其他方法來改變該信息。
您可能希望更改包含在MethodInvocation類中的信息,以便在使用proceed()調(diào)用被截獲的方法之前對被截獲方法的參數(shù)設(shè)置新值。
通過對MethodInvocation對象調(diào)用getArguments()方法,然后在返回的數(shù)組中設(shè)置其中的一個參數(shù)對象,最初傳遞給被截獲的方法的參數(shù)可以被更改。
如果IbusinessClass和BusinessLogic類的foo()方法被更改為使用整型參數(shù),那么就可以將傳遞給被截獲的調(diào)用的值由在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;
}
}
在本例中,被截獲的方法的第一個形參被假設(shè)為int。實(shí)參本身是作為對象傳遞的,所以通過將其包裝在Integer類實(shí)例中的方法,基本的int類型的形參被改為對應(yīng)數(shù)組中的新值。如果您將該參數(shù)設(shè)置為一個非Integer對象的值,那么在運(yùn)行時就會拋出IllegalArgumentException異常。
您還將注意到,invoke(..)方法必須包含一個return語句,因?yàn)樵摲椒ㄐ枰祷刂怠5牵恢貙懙膄oo()方法并不返回對象,所以invoke(..)方法可以以返回null結(jié)束。如果在foo()方法不需要的情況下,您仍然返回了一個對象,那么該對象將被忽略。
如果foo()方法確實(shí)需要返回值,那么需要返回一個與foo()方法的初始返回類型在同一個類或其子類中的對象。如果foo()方法返回一個簡單類型,例如,一個integer,那么您需要返回一個Integer類的對象,當(dāng)方法被重寫時,該對象會自動由AOP代理拆箱,如圖5所示:

圖5. around通知的裝箱和自動拆箱
圖字:
Object invoke:對象調(diào)用
The integer return value is boxed in a Integer object in the AroundAdvice and then unboxed by the AOP Proxy:整型返回值被裝箱在AroundAdvic通知的一個Integer對象中,然后由AOP代理拆箱。
面向方面編程還是一個比較新的領(lǐng)域,尤其是與衍生出它的面向?qū)ο缶幊滔啾取TO(shè)計模式通常被認(rèn)為是常見問題的通用解決方案,因?yàn)槊嫦蚍矫姘l(fā)展的時間還不長,所以已發(fā)現(xiàn)的面向方面設(shè)計模式比較少。
此處要介紹的是一種正在浮現(xiàn)的模式,即Cuckoo's Egg設(shè)計模式。該模式還有其他的叫法,它在面向?qū)ο箢I(lǐng)域的對等體包括模仿對象(Mock Object)和模仿測試(Mock Testing),甚至代理模式也與它有一些類似之處。
Cuckoo's Egg面向方面設(shè)計模式可以被定義為應(yīng)用程序上下文中功能部件的透明和模塊化的置換。就像杜鵑偷偷地把自己的蛋放在另一種鳥的巢中一樣,Cuckoo's Egg設(shè)計模式用一個替代功能部件實(shí)現(xiàn)置換現(xiàn)有的功能部件,而使造成的干擾盡可能少。
這種置換的實(shí)現(xiàn)方式可以是靜態(tài)的、動態(tài)的、部分的、完全的,針對一個對象的多個部分,或針對多個組件。使用面向方面的方法可以透明地實(shí)現(xiàn)功能部件的置換,而無需對應(yīng)用程序的其余部分進(jìn)行更改。要置換應(yīng)用程序中現(xiàn)有功能部件的替代功能部件就是“杜鵑的蛋”。圖6顯示了Cuckoo's Egg設(shè)計模式中的主要組成元素。

圖6. Cuckoo's Egg設(shè)計模式中的主要組成元素
圖字:
Application:應(yīng)用程序
Component:組件
Replacement Feature:替代功能部件
Component 1 and 2 together encompass a distinct feature of the software:組件1和2共同包含了軟件的一個獨(dú)立的功能部件
The Cuckoo's Egg pattern transparently replaces an existing feature of the software:Cuckoo's Egg模式透明地置換了軟件現(xiàn)有的功能部件
Before the pattern is applied:應(yīng)用該模式前
After the pattern is applied:應(yīng)用該模式后
Cuckoo's Egg設(shè)計模式依賴于around通知的概念。您需要借助于積極的和侵入性的around通知來截獲并有效置換應(yīng)用程序中現(xiàn)有的功能部件。
有關(guān)Cuckoo's Egg設(shè)計模式的更多信息,以及AspectJ中的一個可選實(shí)現(xiàn),請參見《AspectJ Cookbook》(O'Reilly,2004年12月出版)。
要使用Spring AOP實(shí)現(xiàn)Cuckoo's Egg設(shè)計模式,需要聲明一個around通知來截獲所有對要置換的功能部件的調(diào)用。與hot-swappable target sources(Spring AOP的一個功能部件,將在本系列的另一篇文章中介紹)不同,around通知的顯式使用使得Cuckoo's Egg實(shí)現(xiàn)可以有效地跨越對象邊界(因此也可以跨越bean邊界)進(jìn)行整個功能部件的置換,如圖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的簡單應(yīng)用程序,其中有一個功能部件跨越了該應(yīng)用程序的多個方面。要置換的功能部件可以被視為包含IBusinessLogic bean中的foo()方法和IBusinessLogic2 bean中的bar()方法。IBusinessLogic2 bean中的baz()方法不是 該功能部件的一部分,所以不進(jìn)行置換。
public interface IBusinessLogic
{
public void foo();
}
public interface IBusinessLogic2
{
public void bar();
public void baz();
}
該例子的完整源代碼可在本文末尾的參考資料小節(jié)中下載。
此處,ReplacementFeature類扮演了“杜鵑的蛋”的角色,它提供了將被透明地引入應(yīng)用程序的替代實(shí)現(xiàn)。ReplacementFeature類實(shí)現(xiàn)了所有在該類引入時要被置換的方法。
public class ReplacementFeature
{
public void foo()
{
System.out.println(
"Inside ReplacementFeature.foo()");
}
public void bar()
{
System.out.println(
"Inside ReplacementFeature.bar()");
}
}
現(xiàn)在需要聲明一個around通知來截獲對跨越bean的功能部件的方法調(diào)用。CuckoosEgg類提供了某種around通知來檢查被截獲的方法,并將適當(dāng)?shù)姆椒ㄕ{(diào)用傳遞給ReplacementFeature類的實(shí)例。
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;
}
}
因?yàn)榕cSpring框架關(guān)系密切,Cuckoo's Egg設(shè)計的詳細(xì)信息被放在springconfig.xml配置文件中。對springconfig.xml文件的更改將確保所有對IbusinessLogic和IBusinessLogic2 bean的foo()方法和bar()方法的調(diào)用都將被截獲,并傳遞給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"/>
...
當(dāng)使用修改后的springconfig.xml文件運(yùn)行例子應(yīng)用程序時,要替換的、被指定為功能部件的一部分的方法調(diào)用完全被截獲并傳遞給ReplacementFeature類。
通常,即使在同一個實(shí)現(xiàn)環(huán)境中,我們也可以用不同的方法來實(shí)現(xiàn)同一種設(shè)計模式。實(shí)現(xiàn)上例的另一種方法是實(shí)現(xiàn)兩個獨(dú)立的通知。
最后需要注意的是,使用Cuckoo's Egg設(shè)計模式置換的功能部件,不管它是跨越bean的還是在一個類中,它的生命周期與它所置換的功能部件的目標(biāo)生命周期匹配。在上例中這沒什么問題,因?yàn)橹挥幸粋€功能部件實(shí)例被置換了,而且唯一的Cuckoo's Egg通知只維護(hù)一個替代功能部件。
這個例子非常簡單,而在實(shí)踐中,您很可能必須處理大量需要用各自的Cuckoo's Egg實(shí)例置換的功能部件實(shí)例。在這種情況下,單個的方面實(shí)例需要被關(guān)聯(lián)到單個的要置換的功能部件實(shí)例。本系列的下一篇文章將會考慮方面生命周期的用法,屆時將解決這個問題。
結(jié)束語
本文介紹了如何在Spring框架內(nèi)謹(jǐn)慎使用around形式的通知。around形式的通知常用于實(shí)現(xiàn)Cuckoo's Egg設(shè)計模式時,所以我們引入了一個例子來說明如何使用Spring AOP實(shí)現(xiàn)這種面向方面設(shè)計模式。
在本系列的第三部分中,您將看到如何使用Spring框架中其他的AOP基本概念。這些概念包括:控制方面生命周期、使用基于introduction通知的積極方面改變應(yīng)用程序的靜態(tài)結(jié)構(gòu),以及使用control flow切入點(diǎn)實(shí)現(xiàn)對方面編織的更細(xì)微的控制。
參考資料
原文出處
An Introduction to Aspect-Oriented Programming with the Spring Framework, Part 2 http://www.onjava.com/pub/a/onjava/2004/10/20/springaop2.html
?作者簡介 |
| Russell Miles是General Dynamics UK公司的一名軟件工程師,他負(fù)責(zé)Java和分布式系統(tǒng),但是他目前主要的興趣在面向方面領(lǐng)域,尤其是AspectJ。 |