面向方面開發人員可以采用的高級技術
簡介:依賴項插入和面向方面編程是互補的技術,所以想把它們結合在一起 使 用是很自然的。請跟隨 Adrian Colyer 一起探索兩者之間的關系,並了解怎樣 才 能把它們組合在一起,來促進高級的依賴項插入場景。
依賴項插入和面向方面編程(AOP)是兩個關鍵的技術,有助於在企業應用程 序中簡化和純化域模型和應用程序分層。依賴項插入封裝了資源和協調器發現的 細節,而方面可以(在其他事情中)封裝中間件服務調用的細節 —— 例如,提 供事務和安全性管理。因為依賴項插入和 AOP 都會形成更簡單、更容易測試的 基 於對象的應用程序,所以想把它們結合在一起使用是很自然的。方面可以幫助把 依賴項插入的能力帶到更廣的對象和服務中,而依賴項插入可以用來對方面本身 進行配置。
在這篇文章中,我將介紹如何把 Spring 框架的依賴項插入與用 AspectJ 5 編寫的方面有效地結合在一起。我假設您擁有基本的 AOP 知識(如果沒有這方 面 知識 ,可以在 參考資料 中找到一些良好的起點),所以我的討論將從對基於 依 賴項插入的解決方案中包含的關鍵角色和職責的分析開始。從這裡,我將介紹如 何通過依賴項插入配置單體(singleton)方面。因為配置非單體方面與配置域 對 象共享許多公共內容,所以後面我會研究一個應用於這兩者的簡單解決方案。總 結這篇文章時,我會介紹如何為多個高級依賴項插入場景使用方面,其中包括基 於接口的插入和重復插入。
請參閱 下載 獲得文章的源代碼,參閱 參考資料 下載 AspectJ 或 Spring 框架,運行示例需要它們。
什麼是依賴項插入?
在 Domain-Driven Design 一書中,Eric Evans 討論了如何把對象與建立對 象的配置和關聯的細節隱藏起來:
對象的大部分威力在於對象內部復雜的配置和關聯。應當對對象進行提煉, 直 到與對象的意義或者在交互中支持對象的作用無關的東西都不存在為止。這個中 間循環的責任很多。如果讓復雜對象負責自己的創建,就會出現問題。
Evans 接著提供了一個汽車引擎的示例:它的眾多部件一起協作,執行引擎 的 職責。雖然可以把引擎塊想像成把一組活塞插入氣缸,但是這樣的設計會把引擎 明顯地弄復雜。相反,技工或機器人裝配引擎,引擎本身只考慮自己的操作。
雖然這個示例是我從書中介紹用於復雜對象創建的工廠 概念一節中取出的, 但是我們也可以用這個概念解釋依賴項插入技術的動機。
從協作到合約
針對這篇文章的目的,可以把依賴項插入想像成對象和對象的執行環境之間 的 合約。對象(執行 ResourceConsumer、 Collaborator 和 ServiceClient 的其 中一個角色或全部角色)同意不出去搜索自己需要的資源、它與之協作的合作伙 伴或它使用的服務。相反,對象提供一種機制,讓這些依賴項可以提供給它。接 下來,執行環境同意在對象需要它的依賴項之前,向對象提供所有的依賴項。
解析依賴項的方法在不同的場景中各有不同。例如,在單元測試用例中,對 象 的執行環境是測試用例本身,所以測試設置代碼有責任直接滿足依賴項。在集成 測試或應用程序在生產環境時,代理 負責尋找滿足對象依賴項的資源,並把它 們 傳遞給對象。代理的角色通常是由輕量級容器扮演的,例如 Spring 框架。不管 依賴項是如何解析的,被配置的對象通常不知道這類細節。在第二個示例中,它 可能還不知道代理的存在。
代理(例如 Spring 框架)有四個關鍵職責,在整篇文章中我將不斷提到這 些 職責,它們是:
確定對象需要配置(通常因為對象剛剛創建)
確定對象的依賴項
發現滿足這些依賴項的對象
用對象的依賴項對它進行配置
從下面的各種依賴項插入解決方案可以看出,解決這些職責有多種策略。
使用 Spring 進行依賴項插入
在標准的 Spring 部署中,Spring 容器同時負責創建和配置核心應用程序對 象(稱為 bean)。因為容器既創建對象,又扮演代理的角色,所以對 Spring 容 器來說,確定 bean 已經創建而且需要配置是件輕而易舉的小事。通過查詢應用 程序的元模型,可以確定 bean 的依賴項,元模型通常是在 Spring 的配置文件 中用 XML 表示的。
滿足 bean 的依賴項的對象是容器管理的其他 bean。容器充當這些 bean 的 倉庫,所以可以用名稱查詢它們(或者在需要的時候創建)。最後,容器用新 bean 的依賴項對其進行配置。這通常是通過 setter 插入完成的(調用新 bean 的 setter 方法,把依賴項作為參數傳遞進去),雖然 Spring 支持其他形式的 插入,例如構造函數插入和查詢方法插入(請參閱 參考資料 學習關於使用 Spring 進行依賴項插入的更多內容。)
方面的依賴項插入
像其他對象一樣,方面可以從通過依賴項插入進行的配置中受益。在許多情 況 下,把方面實現為輕量級控制器 是良好的實踐。在這種情況下,方面確定什麼 時 候應當執行某些行為,但是會委托給協作器去執行實際的工作。例如,可以用異 常處理策略對象配置異常處理方面。方面會探測出什麼時候拋出了異常,並委托 處理器對異常進行處理。清單 1 顯示了基本的 RemoteException 處理方面:
清單 1. RemoteException 處理方面
public aspect RemoteExceptionHandling {
private RemoteExceptionHandler exceptionHandler;
public void setExceptionHandler(RemoteExceptionHandler aHandler) {
this.exceptionHandler = aHandler;
}
pointcut remoteCall() : call(* *(..) throws RemoteException+);
/**
* Route exception to handler. RemoteException will still
* propagate to caller unless handler throws an alternate
* exception.
*/
after() throwing(RemoteException ex) : remoteCall() {
if (exceptionHandler != null)
exceptionHandler.onRemoteException(ex);
}
}
現在我要用依賴項插入,用一個特殊的異常處理策略來配置我的方面。對於 這 個方面,我可以用標准的 Spring 方式,但是有一個警告。一般來說,Spring 既 負責創建 bean,也負責配置 bean。但是,AspectJ 方面是由 AspectJ 運行時 創 建的。我需要 Spring 來配置 AspectJ 創建的方面。對於單體方面最常見的形 式 ,例如上面的 RemoteExceptionHandling 方面,AspectJ 定義了一個 aspectOf () 方法,它返回方面的實例。我可以告訴 Spring 使用 aspectOf() 方法作為 工 廠方法,獲得方面的實例。清單 2 顯示了方面的 Spring 配置:
清單 2. 方面的 Spring 配置
<beans>
<bean name="RemoteExceptionHandlingAspect"
class="org.aspectprogrammer.dw.RemoteExceptionHandling"
factory-method="aspectOf">
<property name="exceptionHandler">
<ref bean="RemoteExceptionHandler"/>
</property>
</bean>
<bean name="RemoteExceptionHandler"
class="org.aspectprogrammer.dw.DefaultRemoteExceptionHandler">
</bean>
</beans>
我想確保我的方面在遠程異常拋出之前得到配置。在示例代碼中,我用 Spring 的 ApplicationContext 確保了這種情況,因為它會自動地預先實例化 所 有單體 bean。如果我使用普通的 BeanFactory,然後再調用 preInstantiateSingletons,也會實現同樣的效果。
域對象的依賴項插入
配置單體方面就像在 Spring 容器中配置其他 bean 一樣簡單,但是對於擁 有 其他生命周期的方面來說,該怎麼辦呢?例如 perthis、pertarget 甚至 percflow 方面?生命周期與單體不同的方面實例,不能由 Spring 容器預先實 例 化;相反,它們是由 AspectJ 運行時根據方面聲明創建的。迄今為止,代理 (Spring)已經知道了對象需要配置,因為它創建了對象。如果我想執行非單體 方面的依賴項插入,就需要用不同的策略來確定需要配置的對象已經創建。
非單體方面不是能夠從外部配置受益的、在 Spring 容器的控制之外創建的 惟 一對象類型。例如,需要訪問倉庫、服務和工廠的域實體(請參閱 參考資料) 也 會從依賴項插入得到與容器管理的 bean 能得到的同樣好處。回憶一下代理的四 項職責:
確定對象需要配置(通常因為對象剛剛創建)
確定對象的依賴項
發現滿足這些依賴項的對象
用對象的依賴項對它進行配置
我仍然想用 Spring 來確定對象的依賴項,去發現滿足這些依賴項的對象, 並 用對象的依賴項來配置對象。但是,需要另一種方法來確定對象需要配置。具體 來說,我需要一個解決方案,針對那些在 Spring 的容器控制之外,在應用程序 執行過程中的任意一點上創建的對象。
SpringConfiguredObjectBroker
我把 Spring 配置的對象叫作 SpringConfigured 對象。創建新的 SpringConfigured 對象之後的需求就是,應當請求 Spring 來配置它。Spring ApplicationContext 支持的 SpringConfiguredObjectBroker 應當做這項工作 , 如清單 3 所示:
清單 3. @SpringConfigured 對象代理
public aspect SpringConfiguredObjectBroker
implements ApplicationContextAware {
private ConfigurableListableBeanFactory beanFactory;
/**
* This broker is itself configured by Spring DI, which will
* pass it a reference to the ApplicationContext
*/
public void setApplicationContext(ApplicationContext aContext) {
if (!(aContext instanceof ConfigurableApplicationContext)) {
throw new SpringConfiguredObjectBrokerException(
"ApplicationContext [" + aContext +
"] does not implement ConfigurableApplicationContext"
);
}
this.beanFactory =
((ConfigurableApplicationContext)aContext).getBeanFactory();
}
/**
* creation of any object that we want to be configured by Spring
*/
pointcut springConfiguredObjectCreation(
Object newInstance,
SpringConfigured scAnnotation
)
: initialization((@SpringConfigured *).new(..)) &&
this(newInstance) &&
@this(scAnnotation);
/**
* ask Spring to configure the newly created instance
*/
after(Object newInstance, SpringConfigured scAnn) returning
: springConfiguredObjectCreation(newInstance,scAnn)
{
String beanName = getBeanName(newInstance, scAnn);
beanFactory.applyBeanPropertyValues(newInstance,beanName);
}
/**
* Determine the bean name to use - if one was provided in
* the annotation then use that, otherwise use the class name.
*/
private String getBeanName(Object obj, SpringConfigured ann) {
String beanName = ann.value();
if (beanName.equals (“”)) {
beanName = obj.getClass().getName ();
}
return beanName;
}
}
SpringConfiguredObjectBroker 內部
我將依次分析 SpringConfiguredObjectBroker 方面的各個部分。首先,這個方面實現了 Spring 的 ApplicationContextAware 接口。代理方面本身是由 Spring 配置的 (這是它得到對應用程序上下文的引用的方式)。讓方面實現 ApplicationContextAware 接口,確保了 Spring 知道在配置期間向它傳遞一個 到當前 ApplicationContext 的引用。
切點 springConfiguredObjectCreation() 用 @SpringConfigured 標注與任何對象的 初始化連接點匹配。標注和新創建的實例,都在連接點上作為上下文被捕捉到。 最後,返回的 after 建議要求 Spring 配置新創建的實例。bean 名稱被用來查 詢實例的配置信息。我可以以 @SpringConfigured 標注的值的形式提供名稱, 或 者也可以默認使用類的名稱。
方面的實現本身可以是標准庫的一部分( 實 際上 Spring 的未來發行版會提供這樣的方面),在這種情況下,我需要做的全 部工作只是對 Spring 要配置的實例的類型進行標注,如下所示:
@SpringConfigured("AccountBean")
public class Account {
...
}
可以在程序的控制下, 創 建這一類類型的實例(例如,作為數據庫查詢的結果),而且它們會把 Spring 為它們配置的全部依賴項自動管理起來。請參閱 下載 得到這裡使用的 @SpringConfigured 標注的示例。請注意,當我選擇為這個示例使用的標注時( 因為提供 bean 名稱是非常自然的方式),標記器接口使得在 Java™ 1.4 及以下版本上可以使用這種方法。
就像我在這一節開始時討論的, SpringConfigured 技術不僅僅適用於域實例,而且適用於在 Spring 容器的控 制 之外創建的任何對象(對於 Spring 本身創建的對象,不需要添加任何復雜性) 。通過這種方式,可以配置任何方面,而不用管它的生命周期。例如,如果定義 percflow 方面,那麼每次進入相關的控制流程時,AspectJ 都會創建新的方面 實 例,而 Spring 會在每個方面創建的時候對其進行配置。
基於接口的插 入
迄今為止,我使用了 Spring 容器讀取的 bean 定義來確定對象的依賴 項 。這個方案的一個變體采用合約接口,由客戶端聲明它的要求。假設前一節的 Account 實體要求訪問 AccountOperationValidationService。我可以聲明一個 接口,如清單 4 所示:
清單 4. 客戶端接口
public interface AccountOperationValidationClient {
public void setAccountOperationValidationService(
AccountOperationValidationService aValidationService);
}
現在,需要訪問 AccountOperationValidationService 的對象必須實現這個 接口,並把自己聲明為客戶。使用與前一節開發的方面類似的方面,我可以匹配 實現這個接口的客戶對象的所有初始化連接點。由它負責第一個代理職責:確定 什麼時候需要配置對象。第二個職責在接口中被明確表達:必須滿足的依賴項是 驗證服務依賴項。我將用一個方面插入所有客戶驗證服務的依賴項。方面得到合 適服務的最簡單方法就是把服務插入到方面自身!清單 5 顯示了一個示例:
清單 5. 服務插入器方面
/**
* ensure that all clients of the account validation service
* have access to it
*/
public aspect AccountOperationValidationServiceInjector {
private AccountOperationValidationService service;
/**
* the aspect itself is configured via Spring DI
*/
public void setService(AccountOperationValidationService aService) {
this.service = aService;
}
/**
* the creation of any object that is a client of the
* validation service
*/
pointcut clientCreation(AccountOperationValidationClient aClient) :
initialization(AccountOperationValidationClient+.new(..)) &&
this(aClient);
/**
* inject clients when they are created
*/
after(AccountOperationValidationClient aClient) returning :
clientCreation(aClient) {
aClient.setAccountOperationValidationService(this.service);
}
}
這個解決方案提供了兩級控制。服務本身實際的定義是在 Spring 的配置文 件中提供的,就像清單 6 中的 XML 片段示例一樣:
清單 6. 服務插入器配置
<beans>
<bean name="AccountOperationValidationServiceInjector"
class="org.aspectprogrammer.dw.
AccountOperationValidationServiceInjector"
factory-method="aspectOf">
<property name="service">
<ref bean="AccountOperationValidationService"/>
</property>
</bean>
<bean name="AccountOperationValidationService"
class="org.aspectprogrammer.dw.
DefaultAccountOperationValidationService">
</bean>
</beans>
服務的客戶只需要實現 AccountOperationValidationClient 接口,那麼就 會自動用 Spring 定義的服務的當前實例對它們進行配置。
重復插入
在 Spring 中的查詢方法插入查詢方法插入是 Spring 容器支持的一種高級 特性:由容器覆蓋被管理 bean 的抽象或具體方法,返回在容器中查詢另一個命 名 bean 的結果。查詢通常是非單體 bean。查詢依賴項的 bean ,用被查詢 bean 類型所聲明的返回類型,定義查詢方法。Spring 配置文件在 bean 的內部 使用 <lookup-method> 元素告訴 Spring 在調用查詢方法時應當返回什 麼 bean 實例。請參閱 參考資料 學習關於這項技術的更多內容。帶有 HotSwappable 目標源的 Spring AOP 代理提供了另一種方法。
.迄今為止,我介紹的解決方案都是在對象初始化之後立即配置對象。但是, 在某些情況下,客戶需要與之協調的對象在運行的時候變化。例如,通過與系統 進行交互,銷售團隊可以動態地為在線預訂應用程序修改報價策略和座位分配策 略。與報價策略和座位分配策略交互的預訂服務需要的策略實現,應當是預訂時 的實現,而不是預訂服務第一次初始化的時候實現的版本。在這種情況下,可以 把依賴項的插入延遲到客戶第一次需要它的時候,並在每次引用依賴項的時候, 將依賴項的最新版本重新插入客戶。
這個場景的基本技術包括字段級插入或 getter 方法覆蓋。在進入示例之前 ,我要再次強調:我要介紹的插入技術所面向的對象,是在 Spring 容器的控制 之外 創建的。對於 Spring 創建的對象,Spring 容器已經提供了解決這些需求 的簡單機制。
字段級插入
在下面的示例中,可以看出如何為延遲插入或重復插入應用字段級插入。字 段的 get 連接點讓我可以確定什麼時候進行插入,而字段類型可以確定要插入 的依賴項。所以,如果客戶聲明了這樣的一個字段:
private PricingStrategy pricingStrategy;
而在客戶的方法中,發現了 下面的代碼
this.pricingStrategy.price(.....);
那麼代碼在運行時的執行會形成 pricingStrategy 字段的 get() 連接點,我可以用它插入當前報價策略實現, 如清單 7 所示:
清單 7. 字段級插入示例
public aspect PricingStrategyInjector {
private PricingStrategy currentPricingStrategy;
public void setCurrentPricingStrategy(PricingStrategy aStrategy) {
this.currentPricingStrategy = aStrategy;
}
/**
* a client is trying to access the current pricing strategy
*/
pointcut pricingStrategyAccess() :
get(PricingStrategy *) &&
!within(PricingStrategyInjector); // don’t advise ourselves!
/**
* whenever a client accesses a pricing strategy field, ensure they
* get the latest...
*/
PricingStrategy around() : pricingStrategyAccess() {
return this.currentPricingStrategy;
}
}
請參閱 下載 獲得這個技術的實際效果。
服務定位策略
重復插入的一個替代就是用更常規的技術,用服務定位策略技術實現插入客 戶。例如:
public interface PricingStrategyLocator {
PricingStrategy getCurrentPricingStrategy();
}
雖然代價是定義一個額外接口,還會使客戶代碼更長一些,但是 這項技術對於代碼清晰性來說具有優勢。
結束語
在這篇文章中,我把依賴項插入看作對象和對象執行的環境之間的合約。對 象不願意外出尋找自己需要的資源、要協作的合作伙伴或者使用的服務。相反, 對象提供了一種機制,允許把這些依賴項提供給它。然後,在對象需要依賴項之 前,執行環境負責把對象需要的所有依賴項提供給它。
我討論了依賴項插入解決方案的四個關鍵職責,這些是代理代表對象獲取依 賴項時必須解決的問題。最後,我介紹了滿足這些需求的許多不同的技術。顯然 ,如果能夠 用 Spring 容器初始化並配置對象,那麼就應當這麼做。對於在 Spring 容器的控制之外創建的對象,例如一些使用非單體實例化模型的域對象 或方面,我推薦使用 @SpringConfigured 標注或類似的東西。這項技術讓您可 以把全部配置信息完全轉移到外部的 Spring 配置文件中。
在編寫這篇文章的示例時,我采用了 AspectJ 5 的最新裡程碑版(到 2005 年 10 月)和 Spring 1.2.4。請 下載 完整的工作示例,開始體驗我討論的想 法。testsrc 目錄下的測試用例是良好的起點。
代碼下載:http://www.ibm.com/developerworks/cn/java/j- aopwork13.html