本文章是由機器翻譯。

技術最前線

Unity 中的原則插入

Dino Esposito

之前兩篇文章中,我介紹了使用 Microsoft Unity 2.0 進行的面向方面的程式設計 (AOP)。AOP 成型于二十世紀九十年代,是為進一步改進和補充物件導向程式設計 (OOP) 而開發的程式設計技術,這種程式設計技術最近有所更新,並且得到許多控制反轉 (IoC) 庫的支援。Unity 也不例外。AOP 的主要目的是讓開發人員更加有效地處理橫切關注點。AOP 在本質上是解決了下麵的問題:在為某個應用程式設計物件模型的時候,您如何處理代碼的安全、緩存或日誌記錄等方面?這些方面對於實現十分重要,但並不嚴格屬於您所構建的模型中的物件。是否應在設計中大肆納入業務之外的方面?或者,利用其他方面來修飾面向業務的類是否會更好?如果您選擇後者,AOP 基本上可以提供相關語法來定義和附加這些方面。

所謂“方面”,就是橫切關注點的實現。在方面的定義中,您需要指定一些事情。首先,您需要為所實現的關注點提供代碼。在 AOP 術語中,這稱為“建議”。建議應用於某個特定的代碼點,該代碼點可以是方法主體、屬性的 getter/setter,或者也可能是例外處理常式。這個代碼點稱為“聯接點”。最後,在 AOP 術語中,還有一個稱作“切入點”。切入點是聯接點的集合。通常,切入點通過方法名稱和萬用字元由條件進行定義。最終,AOP 在運行時在聯接點前後注入建議代碼。這樣一個建議即與一個切入點建立關聯。

在之前的文章中,我討論了 Unity 的攔截 API。借助攔截 API 可以定義附加到類上的建議。在 Unity 術語中,建議是一個行為物件。通常,行為附加到通過 Unity 的 IoC 機制進行解析的某個類型上,不過攔截機制並不嚴格要求使用 IoC 功能。實際上,您可以配置攔截,使之同樣應用於通過純代碼創建的實例。

行為通過一個實現固定介面(IInterceptionBehavior 介面)的類來體現。該介面有一個名為 Invoke 的方法。通過重寫此方法,您實際上定義了要在常規方法調用之前和/或之後所執行的步驟。除了配置腳本,您也可以使用 Fluent 代碼將行為附加到某個類型。這樣,您所要做的只是定義一個聯接點。那切入點呢?

我們上個月討論過,目標物件上所有被攔截的方法都依照行為物件的 Invoke 方法中所表達的邏輯執行。基本攔截 API 不能區分不同方法,並且不支援特定的匹配規則。要做到這一點,您可以求助於策略注入 API。

策略注入和 PIAB

如果您用過 Microsoft Enterprise Library (EntLib) 最新版本 5.0 之前的版本,那麼您有可能聽說過 Policy Injection Application Block (PIAB),並且您還可能在自己的某些應用程式中用過 PIAB。EntLib 5.0 也有 PIAB 模組。那麼 Unity 策略注入與 EntLib PIAB 之間的區別何在?

在 EntLib 5.0 中,PIAB 主要出於相容方面的原因而存在。在新版本中,PIAB 程式集的內容發生了變化。具體而言,攔截的所有功能元件現在都已成為 Unity 的組成部分,並且先前 EntLib 版本中的所有系統提供的調用處理程式都已轉移至其他程式集,如圖 1 所示。

圖 1 Microsoft Enterprise Library 5.0 中對調用處理程式的重構

調用處理程式 Enterprise Library 5.0 中的新程式集
授權處理常式 Microsoft.Practices.EnterpriseLibrary.Security.dll
緩存處理的處理常式 從 PIAB 中刪除
異常處理的處理常式 Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.dll
日誌記錄處理常式 Microsoft.Practices.EnterpriseLibrary.Logging.dll
效能計數器處理常式 Microsoft.Practices.EnterpriseLibrary.PolicyInjection.dll
驗證處理常式 Microsoft.Practices.EnterpriseLibrary.Validation.dll

圖 1 所示,每個調用處理程式都已移至關聯應用區塊的程式集。因此,異常處理調用處理程式移至異常處理應用區塊,而驗證處理常式移至驗證應用區塊,依此類推。其中唯一的例外是效能計數器處理常式,它移入了 PolicyInjection 程式集。儘管程式集發生了變化,但類的命名空間仍然保持不變。另外值得注意的是,出於安全方面的原因,之前位於 PIAB 中的緩存調用處理程式已從 EntLib 5.0 中刪除,而只能從 EntLib Contrib CodePlex 網站獲得:bit.ly/gIcP6H。這些變化的影響是 PIAB 現在由舊的元件組成,這些元件只是為了向後相容而存在,並且仍需一些代碼變更才能與版本 5.0 相容。除非您對舊版本依賴嚴重,否則建議您升級策略注入層,以便利用已融入 Unity 應用區塊的新的(且大體類似的)策略注入 API。讓我們深入探討一下 Unity 中的策略注入。

策略注入概覽

策略注入指的是一層代碼,它擴展基本 Unity 攔截 API,以針對每個方法添加映射規則和調用處理程式。策略注入實現為一種特別的攔截行為,分兩個主要階段:初始化和執行時。

在初始化階段,框架首先確定哪個可用策略可以應用於所攔截的目標方法。在這裡,策略是一組操作,可以按照特定順序注入到所攔截的物件與它的實際調用方之間。您只能攔截針對策略注入顯式配置的物件上的方法(現有實例或新建的實例均可)。

確定適用策略清單之後,策略注入框架開始準備操作管道(操作稱為調用處理程式)。管道通過組合為每個匹配策略而定義的所有處理常式而形成。管道中的處理常式根據策略順序進行排序,並且父策略中的每個處理常式都分配有優先順序。在調用某個啟用策略的方法時,將處理之前構建的管道。如果該方法轉而調用同一物件上其他啟用策略的方法,這些方法的處理常式管道將合併到主管道中。

調用處理程式

調用處理程式比“行為”更加具體,並且因為最初在 AOP 中定義而看起來與建議十分相似。行為應用於類型,由您負責為不同的方法採取不同的操作,而調用處理程式則針對每個方法進行指定。

調用處理程式形成于管道中,並按預先確定的順序接受調用。每個處理常式能夠訪問調用的詳細資訊,包括方法名稱、參數、返回值和預期返回類型。調用處理程式還可以修改參數和返回值、停止調用在管道中的傳播以及引發異常。

值得注意的是,Unity 並不附帶提供任何調用處理程式。您只能自己創建調用處理程式,或從 EntLib 5.0 引用應用區塊並使用圖 1 中列出的任何調用處理程式。

調用處理程式是一個實現 ICallHandler 介面的類,例如:

public interface ICallHandler
{
  IMethodReturn Invoke( 
    IMethodInvocation input,    
    GetNextHandlerDelegate getNext);
  int Order { get; set; }
}

Order 屬性工作表示此處理程式相對於所有其他處理常式的優先順序。 Invoke 方法返回一個包含該方法的任何返回值的類實例。

調用處理程式只是執行自己的具體工作,然後釋放管道,在這個意義上,調用處理程式的實現十分簡單。 為了將控制權轉交給管道中的下一個處理常式,處理常式會調用從 Unity 運行時收到的 getNext 參數。 getNext 參數是一個委託,定義為:

public delegate InvokeHandlerDelegate GetNextHandlerDelegate();

而 InvokeHandlerDelegate 定義為:

public delegate IMethodReturn InvokeHandlerDelegate(    
  IMethodInvocation input,    
  GetNextHandlerDelegate getNext);

Unity 文檔提供了一個闡述攔截的清晰圖表。在圖 2 中,您可以看到一個略加修改的圖表,它表示了策略注入的體系結構。

圖 2 Unity 策略注入中的調用處理程式管道

在系統提供的策略注入行為範圍之內,您可以看到處理程式鏈用以處理對代理物件或派生類所調用的某個給定方法。為了完整概述 Unity 中的策略注入,我們需要瞭解一下匹配規則。

匹配規則

通過匹配規則,您可以指定在哪裡應用攔截邏輯。如果您使用了行為,您的代碼將會應用於整個物件;利用一條或更多匹配規則可以定義篩選器。匹配規則表示用以選擇特定物件和成員的條件,Unity 將為這些物件和成員附加處理常式管道。用 AOP 術語來說,匹配規則就是用以定義切入點的條件。圖 3 列出了 Unity 提供本機支援的匹配規則。

圖 3 Unity 2.0 中受支援匹配規則的清單

匹配規則 說明
AssemblyMatchingRule 基於指定程式集中的類型選擇目標物件。
CustomAttributeMatchingRule 基於成員級別的自訂屬性選擇目標物件。
MemberNameMatchingRule 基於成員名稱選擇目標物件。
MethodSignatureMatchingRule 基於簽名選擇目標物件。
NamespaceMatchingRule 基於命名空間選擇目標物件。
ParameterTypeMatchingRule 基於某個成員的某個參數的類型名稱選擇目標物件。
PropertyMatchingRule 基於成員名稱(包括萬用字元)選擇目標物件。
ReturnTypeMatchingRule 基於返回類型選擇目標物件。
TagMatchingRule 基於臨時 Tag 屬性的賦值選擇目標物件。
TypeMatchingRule 基於類型名稱選擇目標物件。

匹配規則是實現 IMatchingRule 介面的類。瞭解這一點之後,讓我們來看看如何使用策略注入。定義策略主要有三種方式:使用屬性,使用 Fluent 代碼,以及通過配置。

通過屬性添加策略

圖 4 是一個示例調用處理程式,它在某個操作得到負值結果時引發異常。我會在不同情況下使用這個處理常式。

圖 4 NonNegativeCallHandler 類

public class NonNegativeCallHandler : ICallHandler
{
  public IMethodReturn Invoke(IMethodInvocation input,
                              GetNextHandlerDelegate getNext)
  {
    // Perform the operation
    var methodReturn = getNext().Invoke(input, getNext);
    // Method failed, go ahead
    if (methodReturn.Exception != null)
    return methodReturn;
    // If the result is negative, then throw an exception
    var result = (Int32) methodReturn.ReturnValue;
    if (result <0)
    {
      var exception = new ArgumentException("...");
      var response = input.CreateExceptionMethodReturn(exception);
      // Return exception instead of original return value
      return response;
    }
    return methodReturn;
  }
  public int Order { get; set; }
}

使用處理程式最為簡單的方式是在您所認為它可以發揮作用的位置將之附加至方法。 為此,您需要一個屬性,例如:

public class NonNegativeCallHandlerAttribute : HandlerAttribute
{
  public override ICallHandler CreateHandler(    
    IUnityContainer container)
  {
    return new NonNegativeCallHandler();
  }
}

下麵是一個示例 Calculator 類,為之附加了基於屬性的策略:

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

  [NonNegativeCallHandler]
  public Int32 Sub(Int32 x, Int32 y)
  {
    return x - y;
  }
}

其結果是不論返回值是什麼,對於方法 Sum 的調用都會照常執行,而如有負數返回,對於方法 Sub 的調用則會引發異常。

使用 Fluent 代碼

如果您不喜歡屬性,也可以通過 Fluent API 來表達相同的邏輯。 這種情況下,在匹配規則方面須提供更多詳細資訊。 我們來看看如何表達這樣一種想法:只在返回 Int32 並且名為 Sub 方法中注入代碼。 我們使用 Fluent API 來配置 Unity 容器(參見圖 5)。

圖 5 用以定義一組匹配規則的 Fluent 代碼

public static UnityContainer Initialize()
{ // Creating the container
  var container = new UnityContainer();
  container.AddNewExtension<Interception>();

  // Adding type mappings
  container.RegisterType<ICalculator, Calculator>(
    new InterceptionBehavior<PolicyInjectionBehavior>(),
    new Interceptor<TransparentProxyInterceptor>());

  // Policy injection
  container.Configure<Interception>()
    .AddPolicy("non-negative")
    .AddMatchingRule<TypeMatchingRule>(
      new InjectionConstructor(
        new InjectionParameter(typeof(ICalculator))))
    .AddMatchingRule<MemberNameMatchingRule>(
      new InjectionConstructor(
        new InjectionParameter(new[] {"Sub", "Test"})))
    .AddMatchingRule<ReturnTypeMatchingRule>(
      new InjectionConstructor(
        new InjectionParameter(typeof(Int32))))
    .AddCallHandler<NonNegativeCallHandler>(
      new ContainerControlledLifetimeManager(),
        new InjectionConstructor());

  return container;
}

請注意,如果您使用 ContainerControlledLifetimeManager 管理器,則所有方法必定會共用同一個調用處理程式實例。

這段代碼的效果是任何實現 ICalculator 的具體類型(即配置為接受攔截並通過 Unity 進行解析)都會選擇兩個潛在的注入候選物件:Sub 方法和 Test 方法。 然而,只有返回類型為 Int32 的方法才會繼續匹配其他匹配規則。 這就是說,如果 Test 返回雙精度值,它便會遭到排除。

通過配置添加策略

最後,可以通過設定檔表達同樣的理念。 圖 6 是 <unity>節的預期內容。

圖 6 在設定檔中準備策略注入

public class NonNegativeCallHandler : ICallHandler
{
  public IMethodReturn Invoke(IMethodInvocation input, 
                              GetNextHandlerDelegate getNext)
   {
     // Perform the operation  
     var methodReturn = getNext().Invoke(input, getNext);

     // Method failed, go ahead
     if (methodReturn.Exception != null)
       return methodReturn;

     // If the result is negative, then throw an exception
     var result = (Int32) methodReturn.ReturnValue;
     if (result <0)
     {
       var exception = new ArgumentException("...");
       var response = input.CreateExceptionMethodReturn(exception);

       // Return exception instead of original return value
       return response;   
     }

     return methodReturn;
   }

    public int Order { get; set; }
}
<unity xmlns="https://schemas.microsoft.com/practices/2010/unity">
  <assembly name="PolicyInjectionConfig"/>
  <namespace name="PolicyInjectionConfig.Calc"/>
  <namespace name="PolicyInjectionConfig.Handlers"/>

  <sectionExtension  ...
/>

  <container>
    <extension type="Interception" />

    <register type="ICalculator" mapTo="Calculator">
      <interceptor type="TransparentProxyInterceptor" />
      <interceptionBehavior type="PolicyInjectionBehavior" />
    </register>

    <interception>
      <policy name="non-negative">
        <matchingRule name="rule1" 
          type="TypeMatchingRule">
          <constructor>
             <param name="typeName" value="ICalculator" />
          </constructor>
        </matchingRule>
        <matchingRule name="rule2" 
          type="MemberNameMatchingRule">
          <constructor>
            <param name="namesToMatch">
              <array type="string[]">
                <value value="Sub" />
              </array>
            </param>
          </constructor>
        </matchingRule>
        <callHandler name="handler1" 
          type="NonNegativeCallHandler">
          <lifetime type="singleton" />
        </callHandler>                    
      </policy>
    </interception>
            
  </container>
</unity>

結果表明,如果在一個策略中有多個匹配規則,最終結果是對所有規則應用布林運算子 AND(也就是說,全都必須為真)。如果定義了多個策略,對於每個策略都會進行獨立的匹配評估和處理常式應用。因而,您可以從不同的策略應用處理程式。

攔截概覽

總結一下,攔截是 Microsoft .NET Framework 空間中大多數 IoC 框架實現面向方面程式設計的一種方式。通過攔截,您可以在任何特定程式集中任何特定類型的任何特定方法前後運行自己的代碼。以前,EntLib 提供了特定的應用區塊 PIAB 來執行這一工作。在 EntLib 5.0 中,PIAB 的底層引擎已經轉入 Unity,並且實現為 Unity 低級攔截 API 的一種特別行為(對於這種 API 在之前兩期專欄中已有討論)。策略注入行為要求使用 Unity 容器,並且不僅限於通過低級攔截 API 使用。

然而,低級攔截 API 不能用以選擇您要攔截的類型成員,您必須自己編寫代碼來執行這一工作。但利用策略注入行為,您可以將精力集中于所需行為的細節上面,而讓庫根據您所提供的規則來確定行為應用於哪些方法。

Dino Esposito是《Programming Microsoft ASP.NET MVC》(Microsoft Press,2010)一書的作者,也是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。Esposito 定居於義大利,經常在世界各地的業內活動中發表演講。您可訪問他的博客,網址為 weblogs.asp.net/despos

衷心感謝以下技術專家對本文的審閱: Chris Tavares