本文將對AOP攔截在Byteart Retail中的應用進行分析和介紹,同時會介紹AOP兩個應用的具體實現方式,即異常處理與緩存機制的實現。
背景
就一個企業級應用程序而言,實現它的主要目的就是為了解決企業生產過程中出現的實際問題,比如數據問題、管理問題等。因此,應用程序的核心部分就應該是與企業業務相關的部分,也就是我們平時經常提到的“領域模型”。在進行領域模型的建模過程中,根據領域驅動的經驗,需要排除所有與業務無關的成分,以便能夠讓模型能夠完善地表達通用語言的描述,這對於系統分析和團隊合作起着非常重要的作用。所以,在實際的系統設計中,我們都用所謂的POCO或POJO來表述領域模型對象及其之間的關系。所謂POCO/POJO,它們並不是不包含方法(Method)的對象,而相反的是,這些對象不僅包含方法的定義,而且這些方法都是用於處理業務邏輯的,由這些對象類型組成的領域模型稱之為“富領域模型”。與系統中的其它對象的不同之處在於,它們不會“關心”任何與技術相關的東西,比如,它們不知道自己會被以一種什么樣的方式持久化,不知道什么是日志、什么是緩存,如何記錄日志、如何從緩存中獲得需要的其它對象等等。在領域模型中使用POCO/POJO的目的只有一個:領域驅動。
基於這樣的背景,既然領域模型需要維持其“純凈度”,那么對於整個應用程序而言,就需要采用一些框架方面的技術來支持這種需求。AOP攔截就是一種很好的方式(另一種是領域事件,我將會在“領域建模”相關的專題中介紹),通過AOP技術,可以動態產生所攔截類型的代理類型,並在代理類型中對類型操作的輸入、輸出進行一些定制化處理(比如日志記錄等),由此,被代理的類型只需要關注它所需要處理的問題域(也就是我們平時所說的“業務”)即可,對於那些與技術相關的操作,就可以交給代理類的相關方法完成。
這樣做的最大好處,就是關注點分離(Separation of Concerns),也就是上面所說的“被代理類只需要關心問題域即可”,其次就是技術相關處理的靈活性,比如我們可以選擇采用或者不采用日志記錄功能,而無需重新編譯代碼;我們可以動態地選擇緩存解決方案,同樣也無需重新編譯代碼。此外,AOP的采用對於領域模型的單體測試也帶來了很多好處。
Byteart Retail案例中AOP的運用
Byteart Retail案例采用了Microsoft Patterns & Practices Unity作為AOP框架,展示了基於AOP的異常處理和緩存的應用。下面首先讓我們一起看一下在應用程序中如何使用Unity的AOP功能(也就是Interception,以前好像是Policy Injection)。
在應用程序中使用Unity攔截(Interception)
事實上Unity Interception是Unity IoC的一個擴展,跟Interception相關的有兩個單獨的程序集:Microsoft.Practices.Unity.Interception和Microsoft.Practices.Unity.Interception.Configuration。因此,在使用Unity Interception的時候,需要另外引入這兩個程序集。
首先,在Unity的配置信息中,加入sectionExtension節點:
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"/>
然后,對於需要執行AOP攔截的類型注冊,添加攔截器的類型定義以及攔截行為的定義。比如,在Byteart Retail中,所有的AOP攔截操作都是在應用層服務上進行的,因此,Byteart Retail的Unity配置信息中會有類似如下的內容:
<register type="ByteartRetail.ServiceContracts.IUserService, ByteartRetail.ServiceContracts" mapTo="ByteartRetail.Application.Implementation.UserServiceImpl, ByteartRetail.Application"> <interceptor type="InterfaceInterceptor"/> <interceptionBehavior type="ByteartRetail.Infrastructure.InterceptionBehaviors.CachingBehavior, ByteartRetail.Infrastructure"/> <interceptionBehavior type="ByteartRetail.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, ByteartRetail.Infrastructure"/> </register>
在上面的配置信息中,interceptionBehavior節點指定了攔截行為定義,我們很容易看到,Byteart Retail使用了兩種行為:緩存和異常處理。下面會對這兩種攔截行為的具體實現進行介紹。
最后就是攔截行為的定義了。在Byteart Retail中,所有的攔截行為都定義在ByteartRetail.Infrastructure.InterceptionBehaviors命名空間中。基於Unity Interception的攔截行為通常需要實現Microsoft.Practices.Unity.InterceptionExtension.IInterceptionBehavior接口。該接口包含了一個只讀屬性WillExecute以及兩個方法的定義:GetRequiredInterfaces和Invoke,其中最重要的就是Invoke方法。讀者朋友可以參考Byteart Retail案例中CachingBehavior和ExceptionLoggingBehavior的實現來了解這些屬性和方法。
攔截行為:異常處理
Byteart Retail中異常處理的攔截行為實現非常簡單,就是判斷被攔截的方法調用是否存在異常,如果存在,則通過Utils.Log方法將異常記錄到日志中,而在底層,Utils.Log則是使用log4net來實現日志記錄的。異常處理攔截行為的Invoke方法實現如下:
/// <summary> /// 通過實現此方法來攔截調用並執行所需的攔截行為。 /// </summary> /// <param name="input">調用攔截目標時的輸入信息。</param> /// <param name="getNext">通過行為鏈來獲取下一個攔截行為的委托。</param> /// <returns>從攔截目標獲得的返回信息。</returns> public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext) { var methodReturn = getNext().Invoke(input, getNext); if (methodReturn.Exception != null) { Utils.Log(methodReturn.Exception); } return methodReturn; }
於是,當應用層服務調用出現異常時,我們就可以到文件系統中找到相關的異常信息,比如:
在實際項目中,異常處理的方式多種多樣,在此僅通過異常記錄,演示了基於AOP的異常處理方式。
攔截行為:緩存
緩存也是企業級應用程序中一種廣泛使用的非常重要的技術。在Byteart Retail中,緩存不是通過某種特定的緩存機制來直接實現並使用的,而是以Unity Interception的一種攔截行為的方式實現的。緩存的引入,減少了系統的IO壓力(比如減少了網絡流量以及數據庫磁盤讀取次數),從而在一定程度上提高了系統性能。
上文已經大致提到,在Byteart Retail中,由於攔截行為都是基於應用層服務來實現的,因此,緩存也是基於應用層服務來設計的。具體含義是,比如,在引入緩存攔截行為之前,當客戶端調用IProductService.GetProductByID時,應用層服務ProductService就會根據傳入參數從數據庫重建領域對象,然后轉換成數據傳輸對象(DTO)返回;而在引入緩存攔截行為以后,ProductService則會首先查看緩存中是否存在所需的對象,如果存在,則直接返回;否則再到數據庫中查找並返回,同時會將查找到的對象保存到緩存中,以備下次使用。當然在實現之前,最好也對需要緩存的對象進行一些分析,以便更好地實現緩存,比如在Byteart Retail案例中,對產品信息、產品分類等對象進行了緩存,因為這些信息都是查得多,改得少的,因此會對應用程序的響應度有很大的改善。
為了能夠解耦應用程序與緩存機制的具體實現,Byteart Retail使用了代理(Proxy)模式、單件(Singleton)模式和服務定位器(Service Locator)模式設計了整個緩存機制。
首先,定義了一個“緩存供應商(Cache Provider)”的接口,該接口向應用程序提供了訪問緩存機制的方法,包括:向緩存中添加對象、從緩存獲取對象、判斷對象是否存在於緩存等。代碼如下:
/// <summary> /// 表示實現該接口的類型是能夠為應用程序提供緩存機制的類型。 /// </summary> public interface ICacheProvider { #region Methods /// <summary> /// 向緩存中添加一個對象。 /// </summary> /// <param name="key">緩存的鍵值,該值通常是使用緩存機制的方法的名稱。</param> /// <param name="valKey">緩存值的鍵值,該值通常是由使用緩存機制的方法的參數值所產生。</param> /// <param name="value">需要緩存的對象。</param> void Add(string key, string valKey, object value); /// <summary> /// 向緩存中更新一個對象。 /// </summary> /// <param name="key">緩存的鍵值,該值通常是使用緩存機制的方法的名稱。</param> /// <param name="valKey">緩存值的鍵值,該值通常是由使用緩存機制的方法的參數值所產生。</param> /// <param name="value">需要緩存的對象。</param> void Put(string key, string valKey, object value); /// <summary> /// 從緩存中讀取對象。 /// </summary> /// <param name="key">緩存的鍵值,該值通常是使用緩存機制的方法的名稱。</param> /// <param name="valKey">緩存值的鍵值,該值通常是由使用緩存機制的方法的參數值所產生。</param> /// <returns>被緩存的對象。</returns> object Get(string key, string valKey); /// <summary> /// 從緩存中移除對象。 /// </summary> /// <param name="key">緩存的鍵值,該值通常是使用緩存機制的方法的名稱。</param> void Remove(string key); /// <summary> /// 獲取一個<see cref="Boolean"/>值,該值表示擁有指定鍵值的緩存是否存在。 /// </summary> /// <param name="key">指定的鍵值。</param> /// <returns>如果緩存存在,則返回true,否則返回false。</returns> bool Exists(string key); /// <summary> /// 獲取一個<see cref="Boolean"/>值,該值表示擁有指定鍵值和緩存值鍵的緩存是否存在。 /// </summary> /// <param name="key">指定的鍵值。</param> /// <param name="valKey">緩存值鍵。</param> /// <returns>如果緩存存在,則返回true,否則返回false。</returns> bool Exists(string key, string valKey); #endregion }
接下來,緩存機制的具體實現(Byteart Retail提供了兩種實現:基於Microsoft Patterns & Practices Enterprise Library Caching Application Block的緩存以及基於Windows Server Appfabric Caching的緩存,請參考ByteartRetail.Infrastructure.Caching命名空間)以及緩存管理器(Cache Manager)都實現了ICacheProvider接口,緩存管理器結合代理模式和服務定位器模式向應用程序提供緩存服務。相關類型之間的關系可以通過以下類圖表示:
基於這樣的設計,當應用程序需要使用緩存機制時,只需要調用CacheManager的相關方法即可。
接下來讓我們再去了解一下在攔截行為中,緩存是如何使用的。緩存使用的大致過程是,系統首先會根據應用層服務接口中方法上的CachingAttribute特性設置,來判斷當前被攔截方法的緩存使用方式,比如在IProductService.GetProductByID方法上,通過CachingAttribute設置緩存的使用方式是Get,這就意味着當客戶端調用該方法時,攔截行為會根據傳入參數,首先試圖從緩存中獲取對象,如果存在則直接返回;否則,攔截行為會進而調用被代理的應用層服務以獲得對象,將其緩存起來然后返回。再比如在IProductService.DeleteProduct方法上,通過CachingAttribute設置緩存使用方式是Remove,這就意味着當客戶端調用該方法時,攔截行為會將所有在指定方法中存入緩存的對象清除,然后再調用被代理的應用層服務,這就使得下次調用Get的方法時,緩存數據會被更新。比如在IProductService.DeleteProducts方法的定義上,CachingAttribute設置了所有會產生產品信息緩存的方法名稱,而通過這些方法產生的緩存數據將會在DeleteProducts被調用的時候刪除:
/// <summary> /// 刪除商品信息。 /// </summary> /// <param name="productIDs">需要刪除的商品信息的ID值。</param> [OperationContract] [FaultContract(typeof(FaultData))] [Caching(CachingMethod.Remove, "GetProductsForCategory", "GetProductsWithPagination", "GetFeaturedProducts", "GetProductsForCategoryWithPagination", "GetProducts", "GetProductByID")] void DeleteProducts(IDList productIDs);
其實,目前這樣的實現方式還是會有些小問題的,比如一股腦地刪掉所有的緩存對象必然不是最佳的方式,不過Byteart Retail主要是為了演示在AOP攔截中緩存的實現,所以也沒有過細地考慮,讀者朋友有興趣的話可以繼續優化一下。緩存攔截行為CachingBehavior的實現代碼在此就不重復了,讀者朋友請單擊此處查看。最后讓我們看看Byteart Retail在引入不同的緩存機制時,對系統性能的影響。以下是在不使用緩存機制、使用基於Appfabric的緩存以及使用Enterprise Library Caching Application Block的緩存三種情況下,調用IProductService.GetProductByID方法的響應時間對比(平均值),從結果上看,相同調用次數下,使用Enterprise Library Caching Application Block緩存的調用響應時間最短,而不使用緩存機制的調用響應時間最長。Appfabric Caching雖然沒有Caching Application Block的響應時間短,但它能夠提供更為靈活的、分布式的緩存管理解決方案,使得被緩存的數據能夠被跨應用程序域的進程所使用。因此,在實際項目中具體采用何種方案,還需要視情況而定。
總結
本文詳細介紹了Byteart Retail案例中基於AOP攔截的異常處理和緩存的實現方式,這些方式可以供讀者朋友們參考使用,還可以根據這種思想實現更多的Cross-Cutting的功能,例如安全驗證等。最后再回顧一下使用AOP的初衷:我們確實沒有將任何技術內容帶入領域模型中。
【apworks.org站點本文鏈接地址:http://apworks.org/?p=372】