top
Loading...
用AspectJ和Spring進行依賴項插入
依賴項插入和面向方面編程是互補的技術,所以想把它們結合在一起使用是很自然的。請跟隨 Adrian Colyer 一起探索兩者之間的關系,并了解怎樣才能把它們組合在一起,來促進高級的依賴項插入場景。

依賴項插入和面向方面編程(AOP)是兩個關鍵的技術,有助于在企業應用程序中簡化和純化域模型和應用程序分層。依賴項插入封裝了資源和協調器發現的細節,而方面可以(在其他事情中)封裝中間件服務調用的細節 —— 例如,提供事務和安全性管理。因為依賴項插入和 AOP 都會形成更簡單、更容易測試的基于對象的應用程序,所以想把它們結合在一起使用是很自然的。方面可以幫助把依賴項插入的能力帶到更廣的對象和服務中,而依賴項插入可以用來對方面本身進行配置。

在這篇文章中,我將介紹如何把 Spring 框架的依賴項插入與用 AspectJ 5 編寫的方面有效地結合在一起。我假設您擁有基本的 AOP 知識(如果沒有這方面知識 ,可以在 參考資料 中找到一些良好的起點),所以我的討論將從對基于依賴項插入的解決方案中包含的關鍵角色和職責的分析開始。從這里,我將介紹如何通過依賴項插入配置單體(singleton)方面。因為配置非單體方面與配置域對象共享許多公共內容,所以后面我會研究一個應用于這兩者的簡單解決方案。總結這篇文章時,我會介紹如何為多個高級依賴項插入場景使用方面,其中包括基于接口的插入和重復插入。

什么是依賴項插入?

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 的容器控制之外,在應用程序執行過程中的任意一點上創建的對象。

Spring Configured Object Broker

我把 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;       }     }   

Spring Configured Object Broker 內部

我將依次分析 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 會在每個方面創建的時候對其進行配置。

字段級插入

在下面的示例中,可以看出如何為延遲插入或重復插入應用字段級插入。字段的 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 配置文件中。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗