級別: 中級
Adrian Colyer, 首席科學家, Interface21
2006 年 1 月 23 日
依賴項插入和面向方面編程是互補的技術,所以想把它們結合在一起使用是很自然的。請跟隨 Adrian Colyer 一起探索兩者之間的關系,并了解怎樣才能把它們組合在一起,來促進高級的依賴項插入場景。
依賴項插入和面向方面編程
(AOP)是兩個關鍵的技術,有助于在企業應用程序中簡化和純化域模型和應用程序分層。依賴項插入封裝了資源和協調器發現的細節,而方面可以(在其他事情
中)封裝中間件服務調用的細節 —— 例如,提供事務和安全性管理。因為依賴項插入和 AOP
都會形成更簡單、更容易測試的基于對象的應用程序,所以想把它們結合在一起使用是很自然的。方面可以幫助把依賴項插入的能力帶到更廣的對象和服務中,而依
賴項插入可以用來對方面本身進行配置。
在這篇文章中,我將介紹如何把 Spring 框架的依賴項插入與用 AspectJ 5 編寫的方面有效地結合在一起。我假設您擁有基本的 AOP 知識(如果沒有這方面知識 ,可以在 參考資料
中找到一些良好的起點),所以我的討論將從對基于依賴項插入的解決方案中包含的關鍵角色和職責的分析開始。從這里,我將介紹如何通過依賴項插入配置單體
(singleton)方面。因為配置非單體方面與配置域對象共享許多公共內容,所以后面我會研究一個應用于這兩者的簡單解決方案。總結這篇文章時,我會
介紹如何為多個高級依賴項插入場景使用方面,其中包括基于接口的插入和重復插入。
|
關于這個系列
AOP@Work
系列面對的是在面向方面編程上有些基礎,想擴展或加深了解的開發人員。同 developerWorks 的大多數文章一樣,這個系列高度實用:讀完每篇介紹新技術的文章,都可以立即投入實用。
這個系列每個作者的選擇,都是因為他們在面向方面編程領域具有領袖地位或專家水平。許多作者都是系列中介紹的項目和工具的參與者。每篇文章都力圖提供一個中立的評述,以確保這里表達的觀點的公正與正確。
如果有對每個作者文章的評論或問題,請分別與他們聯系。要對這個系列整體進行評論,可以與系列的負責人 Nicholas
Lesiecki 聯系。請參閱 參考資料 獲取關于 AOP 的更多背景資料。
|
|
請參閱 下載 獲得文章的源代碼,參閱 參考資料 下載 AspectJ 或 Spring 框架,運行示例需要它們。
什么是依賴項插入?
在 Domain-Driven Design 一書中,Eric Evans 討論了如何把對象與建立對象的配置和關聯的細節隱藏起來:
對象的大部分威力在于對象內部復雜的配置和關聯。應當對對象進行提煉,直到與對象的意義或者在交互中支持對象的作用無關的東西都不存在為止。這個中間循環的責任很多。如果讓復雜對象負責自己的創建,就會出現問題。
Evans 接著提供了一個汽車引擎的示例:它的眾多部件一起協作,執行引擎的職責。雖然可以把引擎塊想像成把一組活塞插入氣缸,但是這樣的設計會把引擎明顯地弄復雜。相反,技工或機器人裝配引擎,引擎本身只考慮自己的操作。
雖然這個示例是我從書中介紹用于復雜對象創建的工廠 概念一節中取出的,但是我們也可以用這個概念解釋依賴項插入技術的動機。
從協作到合約
|
參考讀物
關于依賴項插入的經典介紹,請參閱 Martin Fowler 的 “Inversion of Control Containers and
the Dependency Injection Pattern”。關于使用 Spring 的依賴項插入的更多內容,請參閱 Professional Java Development with the Spring Framework。這兩者的鏈接都在
參考資料 中。
|
|
針對這篇文章的目的,可以把依賴項插入想像成對象和對象的執行環境之間的合約。對象(執行 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); } }
|
|
研究源代碼
如果想查看 RemoteExceptionHandling 方面配置的實際效果,請下載 文章源代碼 并運行 testsrc 文件夾中的 RemoteExceptionHandlingTest 測試用例。
|
|
現
在我要用依賴項插入,用一個特殊的異常處理策略來配置我的方面。對于這個方面,我可以用標準的 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
目錄下的測試用例是良好的起點。
下載
描述 |
名字 |
大小 |
下載方法 |
Source code |
j-aopwork13.zip |
26 KB |
?FTP |
參考資料
學習
獲得產品和技術
討論
關于作者
|
|
|
Adrian
Colyer 是 Eclipse.org 的 AspectJ 項目的負責人,也是 AJDT 項目的創始人,AJDT 項目在 Eclipse
中為 AspectJ 提供 IDE 支持。在 2005 年 10 月接受 Interface21 的首席科學家一職之前,Adrian
在位于英國 Hursley 的 IBM 軟件實驗室領導面向方面的軟件開發團隊。他是 Eclipse AspectJ 圖書
Aspect-Oriented Programming with AspectJ and the Eclipse AspectJ
Development Tools 的合作者,還經常針對面向方面編程主題進行演講。
|