共用方式為


本文章是由機器翻譯。

領先技術

Unity 中的攔截功能

Dino Esposito

上個月的專欄中,我簡要介紹了 Unity 2.0 依賴關係注入容器使用的攔截機制。在演示面向方面的程式設計 (AOP) 的核心概念之後,我介紹了一個具體的攔截示例,可能符合如今的很多開發人員的需要。

您是否想要擴展現有代碼的行為卻不想以任何方式觸及原始程式碼?您是否希望圍繞現有的代碼再運行更多代碼?

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

為了使您完全理解這篇後續文章的目的,我先概要介紹上個月的內容。您會發現,在上個月的代碼中,我作了一些假設並使用了一些預設元件。這個月我將回過頭去更詳細地討論您通常會遇到的選擇和選項。

Unity 中的 AOP

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

考慮一下稍有不同的情況。首先,如果您並不是獨立顧問而是全職公司員工,該怎麼辦呢?收到的更改請求越多,您就得在現有專案之外花費越多的時間;更糟糕的是,您還得面臨為基本代碼創建新分支(並不是必需的)的風險。因此,您會由衷地喜歡可以讓您順利添加新的行為卻無需觸及原始程式碼的解決方案。

最後,假設有人報告了錯誤或嚴重的性能問題。您需要調查並修正問題,而且您希望它不會引人注意。在這種情況下,您同樣期望能夠順利添加新的行為而不用觸及原始程式碼。

AOP 可以説明您應對所有這些情況。

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

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

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

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

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

可攔截的實例

若要為現有的類實例或新創建的類實例添加新的行為,您必須對工廠有一定的控制力。換句話說,AOP 不是萬能的,您不可能綁定通過標準的 new 運算子產生實體的普通 CLR 類:

var calculator = new Calculator();

AOP 框架控制實例的方式可能大有不同。 在 Unity 中,您可以求助於某些返回原始物件代理的顯式調用,或者讓代碼完全在 IoC 框架之後運行。 為此,大多數 IoC 框架都提供 AOP 功能。 Spring.NET 和 Unity 就是兩個示例。 當 AOP 和 IoC 一起使用時,就會得到順利、輕鬆和有效的編碼體驗。

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

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

最後要處理一個包裝了原始物件的可攔截代理。 在這種情況下,我假設 Calculator 類實現 ICalculator 介面。 若要變得可攔截,類必須實現介面或者繼承自 MarshalByRefObject。 如果類派生自 MarshalByRefObject,那麼攔截程式的類型必須是 TransparentProxyInterceptor:

var calculator = new Calculator();
var calculatorProxy = Intercept.ThroughProxy(calculator,
  new TransparentProxyInterceptor(), new[] { new LogBehavior() });
Console.WriteLine(calculatorProxy.Sum(2, 2));

Intercept 類還提供 NewInstance 方法,您可以調用該方法以更直接的方式創建可攔截的物件。 以下就是使用方法:

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

請注意,當您使用 NewInstance 時,攔截程式元件必須稍有不同。它不能是 InterfaceInterceptor,也不能是 TransparentProxyInterceptor,而應該是 VirtualMethodInterceptor 物件。那麼 Unity 中有多少種攔截程式?

實例和類型攔截程式

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

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

對於實例攔截,應用程式碼首先使用傳統的工廠(或 new 運算子)創建目標物件,然後強制通過 Unity 提供的代理與其交互。

對於類型攔截,應用程式通過 API 或 Unity 創建目標物件,然後處理該實例。(您無法使用 new 運算子直接創建物件並獲得類型攔截。)但是目標物件不是原始類型。實際的類型由 Unity 即時派生,並且會加入攔截邏輯(請參見圖 1)。

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

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

InterfaceInterceptor 可以攔截目標物件上的一個介面的公共實例方法。該攔截程式可以應用到新的和現有的實例。

TransparentProxyInterceptor 可以攔截多個介面和按引用封送的物件上的公共實例方法。這是最慢的攔截方式,但可以攔截的方法最多。該攔截程式可以應用到新的和現有的實例。

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

應該注意的是,實例攔截可以應用到任意公共的實例方法,但不能應用到構造函數。這在將攔截應用到現有實例時相當明顯,而將攔截應用到新創建的實例時則不那麼明顯。實例攔截的實現方式是構造函數在應用程式碼取回要處理的物件時已經執行。結果,任何可攔截操作都必須在創建實例之後。

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

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

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 配置:

// Configure the IoC container
var container = UnityStarter.Initialize();

// Start the application
var calculator = container.Resolve<ICalculator>();
var result = calculator.Sum(2, 2);

啟動容器所需的代碼可以隔離在不同的類中,並在應用程式啟動時調用。 啟動代碼將指導容器如何圍繞應用程式解析類型以及如何處理攔截。 調用 Resolve 方法可以為您遮罩攔截的所有細節。 图 3 顯示了啟動代碼可能的實現方式。

图 3 启动 Unity

public class UnityStarter {
  public static UnityContainer Initialize() {
    var container = new UnityContainer();

    // Enable interception in the current container 
    container.AddNewExtension<Interception>();

    // Register ICalculator with the container and map it to 
    // an actual type.
In addition, specify interception details.
container.RegisterType<ICalculator, Calculator>(
      new Interceptor<VirtualMethodInterceptor>(),
      new InterceptionBehavior<LogBehavior>());

    return container;
  }
}

比較有利的一點是這段代碼可以移動到獨立的程式集中,動態載入或更改。 更重要的是,您可以在一個位置配置 Unity。 如果您堅持使用 Intercept 類(其行為就像智慧工廠,每次使用時都需要做準備),就無法做到這一點。 因此,如果您的應用程式需要 AOP,請務必通過 IoC 容器獲得。 如果將配置的詳細資訊移到 app.config 檔(如果是 Web 應用程式則是 web.config)中,就可以用更靈活的方式實現相同的解決方案。 在這種情況下,啟動代碼包含以下兩行:

var container = new UnityContainer();
container.LoadConfiguration();

图 4 顯示了設定檔中必須包含的腳本。 在這裡,我為 ICalculator 類型註冊了兩種行為。 這表示對介面公共成員的所有調用都將由 LogBehavior 和 BinaryBehavior 進行預處理和後處理。

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

<unity xmlns="https://schemas.microsoft.com/practices/2010/unity">
  <assembly name="SimplestWithConfigIoC"/>
  <namespace name="SimplestWithConfigIoC.Calc"/>
  <namespace name="SimplestWithConfigIoC.Behaviors"/>

  <sectionExtension 
    type="Microsoft.Practices.Unity.
InterceptionExtension.Configuration.
InterceptionConfigurationExtension,     
      Microsoft.Practices.Unity.Interception.Configuration" />

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

    <register type="ICalculator" mapTo="Calculator">
      <interceptor type="InterfaceInterceptor"/>
      <interceptionBehavior type="LogBehavior"/>
      <interceptionBehavior type="BinaryBehavior"/>
    </register>

    <register type="LogBehavior">
    </register>

    <register type="BinaryBehavior">
    </register>

  </container>
</unity>

請注意,由於 LogBehavior 和 BinaryBehavior 是具體的類型,因此您實際上根本不需要註冊它們。 Unity 的預設設置會自動處理它們。

行為

在 Unity 中,行為是真正實現橫切關注點的物件。 作為實現 IInterceptionBehavior 介面的類,行為將覆蓋被攔截方法的執行迴圈,並且可以修改方法參數或返回值。 行為甚至可以完全阻止方法被調用,或者多次調用方法。

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

图 5 行为示例

public class BinaryBehavior : IInterceptionBehavior {
  public IEnumerable<Type> GetRequiredInterfaces() {
    return Type.EmptyTypes;
  }

  public bool WillExecute {
    get { return true; }
  }

  public IMethodReturn Invoke(
    IMethodInvocation input, 
    GetNextInterceptionBehaviorDelegate getNext) {

    // Perform the operation
    var methodReturn = getNext().Invoke(input, getNext);

    // Grab the output
    var result = methodReturn.ReturnValue;

    // Transform
    var binaryString = ((Int32)result).ToBinaryString();

    // For example, write it out
    Console.WriteLine("Rendering {0} as binary = {1}", 
      result, binaryString);

    return methodReturn;
  }
}

這其實有點微妙。 Invoke 總是被調用,因此即使返回 False,您的行為實際上也會執行。 但是在創建代理或派生類型時,如果為該類型註冊的所有行為都將 WillExecute 設置為 False,那麼也就不會創建代理本身,您將再次處理原始物件。 這實際上是在優化代理創建。

GetRequiredInterfaces 方法允許行為向目標物件添加新介面,從此方法返回的介面將添加到代理中。 因此,行為的核心就是 Invoke 方法。 該參數輸入讓您可以訪問目標物件上正在調用的方法。 參數 getNext 是一個委託,用於移動到管道中下一個行為,並且最終執行目標上的方法。

Invoke 方法確定調用目標物件上的公共方法時所用的實際邏輯。 請注意,目標物件上所有被攔截的方法都將按照 Invoke 中表達的邏輯執行。

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

if(input.MethodBase.Name == "Sum") {
  ...
}

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

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

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