Unity 中的攔截功能(轉)

(轉自MSDN雜誌:http://msdn.microsoft.com/zh-cn/magazine/gg535676.aspx)javascript

上個月的專欄中,我簡要介紹了 Unity 2.0 依賴關係注入容器使用的攔截機制。 在演示面向方面的編程 (AOP) 的核心概念以後,我介紹了一個具體的攔截示例,可能符合現在的不少開發人員的須要。java

您是否想要擴展示有代碼的行爲卻不想以任何方式觸及源代碼? 您是否但願圍繞現有的代碼再運行更多代碼?web

AOP 的目標是提供一種方法,將核心代碼與其餘干擾核心業務邏輯的內容隔離開。 Unity 2.0 提供基於 Microsoft .NET Framework 4 的框架來實現此目的,並且極其快速和方便。編程

爲了使您徹底理解這篇後續文章的目的,我先概要介紹上個月的內容。 您會發現,在上個月的代碼中,我做了一些假設並使用了一些默認組件。 這個月我將回過頭去更詳細地討論您一般會遇到的選擇和選項。app

Unity 中的 AOP

假設您已經部署了應用程序,以便在某個時刻執行一些與業務相關的操做。 一天,您的客戶要求擴展該行爲,以便執行更多工做。 您找出源代碼,進行修改,而後按照編碼和測試新功能所需的時間來收取諮詢費用。 但若是您能順利添加新的行爲而不用觸及現有的源代碼,豈不是更好?框架

考慮一下稍有不一樣的狀況。 首先,若是您並非獨立顧問而是全職公司員工,該怎麼辦呢? 收到的更改請求越多,您就得在現有項目以外花費越多的時間;更糟糕的是,您還得面臨爲基本代碼建立新分支(並非必需的)的風險。 所以,您會由衷地喜歡可讓您順利添加新的行爲卻無需觸及源代碼的解決方案。函數

最後,假設有人報告了錯誤或嚴重的性能問題。 您須要調查並修正問題,並且您但願它不會引人注意。 在這種狀況下,您一樣指望可以順利添加新的行爲而不用觸及源代碼。性能

AOP 能夠幫助您應對全部這些狀況。測試

上個月,我演示瞭如何利用 Unity 2.0 中的攔截 API 圍繞現有方法添加預處理和後處理代碼,而不用觸及該方法。 但這段簡短的演示利用了幾個假設。優化

首先,它利用由 Unity 反轉控制 (IoC) 基礎結構註冊的類型,並經過 Unity 工廠層進行實例化。

其次,聯接點集合只是經過接口定義的。 在 AOP 術語中,聯接點集合表明目標類中的位置集合,而框架就在這些位置按需注入額外的行爲。 基於接口的聯接點集合表示只有該接口的成員纔會經過代碼注入在運行時擴展。

第三,我主要關注支持攔截的配置設置,而沒有考慮可以讓您在代碼中配置 Unity 的 Fluent API。

在本文的其他部分,我將探討 Fluent API 以及定義 Unity 攔截功能的其餘方法。

可攔截的實例

若要爲現有的類實例或新建立的類實例添加新的行爲,您必須對工廠有必定的控制力。 換句話說,AOP 不是萬能的,您不可能綁定經過標準的 new 運算符實例化的普通 CLR 類:

  1.  
  2.           var calculator = new Calculator();
  3.         

AOP 框架控制實例的方式可能大有不一樣。 在 Unity 中,您能夠求助於某些返回原始對象代理的顯式調用,或者讓代碼徹底在 IoC 框架以後運行。 爲此,大多數 IoC 框架都提供 AOP 功能。 Spring.NET 和 Unity 就是兩個示例。 當 AOP 和 IoC 一塊兒使用時,就會獲得順利、輕鬆和有效的編碼體驗。

咱們先從一個示例開始,其中沒有使用 IoC 功能。 這裏是一些基本代碼,可讓現有的 Calculator 類實例變得能夠攔截:

  1.  
  2.           var calculator = new Calculator();
  3. var calculatorProxy = Intercept.ThroughProxy<ICalculator>(calculator,
  4.   new InterfaceInterceptor(), new[] { new LogBehavior() });
  5. Console.WriteLine(calculatorProxy.Sum(22));
  6.         

最後要處理一個包裝了原始對象的可攔截代理。 在這種狀況下,我假設 Calculator 類實現 ICalculator 接口。 若要變得可攔截,類必須實現接口或者繼承自 MarshalByRefObject。 若是類派生自 MarshalByRefObject,那麼攔截程序的類型必須是 TransparentProxyInterceptor:

  1.  
  2.           var calculator = new Calculator();
  3. var calculatorProxy = Intercept.ThroughProxy(calculator,
  4.   new TransparentProxyInterceptor(), new[] { new LogBehavior() });
  5. Console.WriteLine(calculatorProxy.Sum(22));
  6.         

Intercept 類還提供 NewInstance 方法,您能夠調用該方法以更直接的方式建立可攔截的對象。 如下就是使用方法:

  1.  
  2.           var calculatorProxy = Intercept.NewInstance<Calculator>(
  3.   new VirtualMethodInterceptor(), new[] { new LogBehavior() });
  4.         

請注意,當您使用 NewInstance 時,攔截程序組件必須稍有不一樣。它不能是 InterfaceInterceptor,也不能是 TransparentProxyInterceptor,而應該是 VirtualMethodInterceptor 對象。 那麼 Unity 中有多少種攔截程序?

實例和類型攔截程序

攔截程序是一種 Unity 組件,該組件負責捕獲對目標對象的原始調用並經過行爲管道進行路由,使得每一個行爲都有機會在常規方法調用以前或以後運行。 攔截的類型有兩種:實例攔截和類型攔截。

實例攔截程序建立代理以篩選針對所攔截實例的傳入調用。 類型攔截程序生成新的類(這個類派生自要攔截的類型),並處理該派生類型的實例。 不用說,原始類型和派生類型的區別就在於用來篩選傳入調用的邏輯。

對於實例攔截,應用程序代碼首先使用傳統的工廠(或 new 運算符)建立目標對象,而後強制經過 Unity 提供的代理與其交互。

對於類型攔截,應用程序經過 API 或 Unity 建立目標對象,而後處理該實例。 (您沒法使用 new 運算符直接建立對象並得到類型攔截。)可是目標對象不是原始類型。 實際的類型由 Unity 實時派生,而且會加入攔截邏輯(請參見圖 1)。

圖 1 實例攔截程序和類型攔截程序

InterfaceInterceptor 和 TransparentProxyInterceptor 是兩個 Unity 攔截程序,屬於實例攔截程序類別。VirtualMethodInterceptor 屬於類型攔截程序類別。

InterfaceInterceptor 能夠攔截目標對象上的一個接口的公共實例方法。 該攔截程序能夠應用到新的和現有的實例。

TransparentProxyInterceptor 能夠攔截多個接口和按引用封送的對象上的公共實例方法。 這是最慢的攔截方式,但能夠攔截的方法最多。 該攔截程序能夠應用到新的和現有的實例。

VirtualMethodInterceptor 能夠攔截公共和受保護的虛擬方法。 該攔截程序只能應用到新的實例。

應該注意的是,實例攔截能夠應用到任意公共的實例方法,但不能應用到構造函數。 這在將攔截應用到現有實例時至關明顯, 而將攔截應用到新建立的實例時則不那麼明顯。 實例攔截的實現方式是構造函數在應用程序代碼取回要處理的對象時已經執行。 結果,任何可攔截操做都必須在建立實例以後。

類型攔截使用動態代碼生成來返回從原始類型繼承的對象。 在這種狀況下,任何公共和受保護的虛擬方法都被重寫,以便支持攔截。 請考慮使用如下代碼:

  1.  
  2.           var calculatorProxy = Intercept.NewInstance<Calculator>(
  3.   new VirtualMethodInterceptor(), new[] { new LogBehavior() });
  4.         

Calculator 類以下所示:

public class Calculator { public virtual Int32 Sum(Int32 x, Int32 y) { return x + y; } }

圖 2 顯示了對 calculatorProxy 變量進行動態檢查後獲得的類型的實際名稱。

圖 2 類型攔截以後的實際類型

另外還要注意實例攔截和類型攔截之間存在的其餘顯著區別,例如按照調用的對象攔截調用。 使用類型攔截時,若是一個方法調用同一對象上的另外一個方法,那麼該自我調用就能夠被攔截,由於攔截邏輯在同一個對象中。 可是,對於實例攔截,則只有當調用經過代理進行時,才能發生攔截。 固然,自我調用不須要通過代理,所以不會發生攔截。

使用 IoC 容器

在上個月的示例中,我使用了 Unity 庫的 IoC 容器來完成對象的建立。 IoC 容器是圍繞對象建立的一個額外層,能夠增長應用程序的靈活性。 若是您將 IoC 框架與更多 AOP 功能相結合,就更是如此。 此外(我是這樣認爲的),若是您將 IoC 容器與離線配置結合使用,代碼的靈活程度將超乎想象。 可是,下面這個示例將使用 Unity 的容器以及基於代碼的 Fluent 配置:

  1.  
  2.           // Configure the IoC container
  3. var container = UnityStarter.Initialize();
  4.  
  5. // Start the application
  6. var calculator = container.Resolve<ICalculator>();
  7. var result = calculator.Sum(22);
  8.         

啓動容器所需的代碼能夠隔離在不一樣的類中,並在應用程序啓動時調用。 啓動代碼將指導容器如何圍繞應用程序解析類型以及如何處理攔截。 調用 Resolve 方法能夠爲您屏蔽攔截的全部細節。 圖 3 顯示了啓動代碼可能的實現方式。

圖 3 啓動 Unity

  1.  
  2.           public class UnityStarter {
  3.   public static UnityContainer Initialize() {
  4.     var container = new UnityContainer();
  5.  
  6.     // Enable interception in the current container 
  7.     container.AddNewExtension<Interception>();
  8.  
  9.     // Register ICalculator with the container and map it to 
  10.     // an actual type.
  11.           In addition, specify interception details.
  12.           container.RegisterType<ICalculator, Calculator>(
  13.       new Interceptor<VirtualMethodInterceptor>(),
  14.       new InterceptionBehavior<LogBehavior>());
  15.  
  16.     return container;
  17.   }
  18. }
  19.         

比較有利的一點是這段代碼能夠移動到獨立的程序集中,動態加載或更改。 更重要的是,您能夠在一個位置配置 Unity。 若是您堅持使用 Intercept 類(其行爲就像智能工廠,每次使用時都須要作準備),就沒法作到這一點。 所以,若是您的應用程序須要 AOP,請務必經過 IoC 容器得到。 若是將配置的詳細信息移到 app.config 文件(若是是 Web 應用程序則是 web.config)中,就能夠用更靈活的方式實現相同的解決方案。 在這種狀況下,啓動代碼包含如下兩行:

  1.  
  2.           var container = new UnityContainer();
  3. container.LoadConfiguration();
  4.         

圖 4 顯示了配置文件中必須包含的腳本。 在這裏,我爲 ICalculator 類型註冊了兩種行爲。 這表示對接口公共成員的全部調用都將由 LogBehavior 和 BinaryBehavior 進行預處理和後處理。

圖 4 經過配置添加攔截細節

  1.  
  2.           <unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
  3.   <assembly name="SimplestWithConfigIoC"/>
  4.   <namespace name="SimplestWithConfigIoC.Calc"/>
  5.   <namespace name="SimplestWithConfigIoC.Behaviors"/>
  6.  
  7.   <sectionExtension 
  8.     type="Microsoft.Practices.Unity.
  9.           InterceptionExtension.Configuration.
  10.           InterceptionConfigurationExtension,     
  11.       Microsoft.Practices.Unity.Interception.Configuration" />
  12.  
  13.   <container>
  14.     <extension type="Interception" />
  15.  
  16.     <register type="ICalculator" mapTo="Calculator">
  17.       <interceptor type="InterfaceInterceptor"/>
  18.       <interceptionBehavior type="LogBehavior"/>
  19.       <interceptionBehavior type="BinaryBehavior"/>
  20.     </register>
  21.  
  22.     <register type="LogBehavior">
  23.     </register>
  24.  
  25.     <register type="BinaryBehavior">
  26.     </register>
  27.  
  28.   </container>
  29. </unity>
  30.         

請注意,因爲 LogBehavior 和 BinaryBehavior 是具體的類型,所以您實際上根本不須要註冊它們。 Unity 的默認設置會自動處理它們。

行爲

在 Unity 中,行爲是真正實現橫切關注點的對象。 做爲實現 IInterceptionBehavior 接口的類,行爲將覆蓋被攔截方法的執行循環,而且能夠修改方法參數或返回值。 行爲甚至能夠徹底阻止方法被調用,或者屢次調用方法。

一個行爲由三個方法組成。 圖 5 顯示了一個攔截方法 Sum 並將返回值修改成二進制字符串的示例行爲。 方法 WillExecute 只是一種優化代理的方式。 若是它返回 False,行爲就不會執行。

圖 5 行爲示例

  1.  
  2.           public class BinaryBehavior : IInterceptionBehavior {
  3.   public IEnumerable<Type> GetRequiredInterfaces() {
  4.     return Type.EmptyTypes;
  5.   }
  6.  
  7.   public bool WillExecute {
  8.     get { return true; }
  9.   }
  10.  
  11.   public IMethodReturn Invoke(
  12.     IMethodInvocation input, 
  13.     GetNextInterceptionBehaviorDelegate getNext) {
  14.  
  15.     // Perform the operation
  16.     var methodReturn = getNext().Invoke(input, getNext);
  17.  
  18.     // Grab the output
  19.     var result = methodReturn.ReturnValue;
  20.  
  21.     // Transform
  22.     var binaryString = ((Int32)result).ToBinaryString();
  23.  
  24.     // For example, write it out
  25.     Console.WriteLine("Rendering {0} as binary = {1}"
  26.       result, binaryString);
  27.  
  28.     return methodReturn;
  29.   }
  30. }
  31.         

這其實有點微妙。 Invoke 老是被調用,所以即便返回 False,您的行爲實際上也會執行。 可是在建立代理或派生類型時,若是爲該類型註冊的全部行爲都將 WillExecute 設置爲 False,那麼也就不會建立代理自己,您將再次處理原始對象。 這其實是在優化代理建立。

GetRequiredInterfaces 方法容許行爲向目標對象添加新接口,今後方法返回的接口將添加到代理中。 所以,行爲的核心就是 Invoke 方法。 該參數輸入讓您能夠訪問目標對象上正在調用的方法。 參數 getNext 是一個委託,用於移動到管道中下一個行爲,而且最終執行目標上的方法。

Invoke 方法肯定調用目標對象上的公共方法時所用的實際邏輯。 請注意,目標對象上全部被攔截的方法都將按照 Invoke 中表達的邏輯執行。

若是要使用更特殊的匹配規則,該怎麼辦呢? 使用我在本文中介紹的普通攔截,您能作的就是運行一組 IF 語句,來找出被調用的是哪一個方法,以下所示:

  1.  
  2.           if(input.MethodBase.Name == "Sum") {
  3.   ...
  4.           }
  5.         

下個月我將繼續這個話題,探討以更有效的方式應用攔截,爲被攔截的方法定義匹配規則。

相關文章
相關標籤/搜索