2011年7月17日 星期日

(轉) .Net平台AOP技術研究

Net平台AOP技術概覽
.Net平台與Java平台相比,由於它至今在服務端仍不具備與unix系統的兼容性,也不具備類似於Java平台下J2EE這樣的企業級容器,使得.Net平台在大型的企業級應用上,常常為人所詬病。就目前而言,.Net平台並沒有提供AOP技術的直接實現,而微軟在未來對於.Net的發展戰略目標,我們仍未可知。但我相信微軟對於目前炙手可熱的AOP技術應該不會視而不見。也許在未來的.Net平台下,會出現類似於Spring那樣的輕量級IoC容器,加上O/R Mapping的進一步實現與完善,隨著Windows Server操作系統的逐步推新,.Net平台對於企業級系統開發的支持會越來越多。
AOP技術在.Net平台中的應用,相較於Java平台而言,還遠不夠成熟,功能也相對較弱,目前能夠投入商用的AOP工具幾乎沒有。借鑒Java開源社區的成功,.Net平台下AOP工具的開發也都依託於開源社區的力量。眾多開源愛好者,仍然在堅持不懈對AOP技術進行研究和實踐,試圖找到AOP技術與.Net之間的完美結合點,從而開發出真正能夠商用的功能強大的AOP工具。就目前而言,大部分在.Net平台下的AOP工具,大部分均脫胎於Java平台下的AOP工具,例如Spring.Net之於Spring,Eos之於AspectJ。由於Java平台和.Net平台在語言機制上的相似性,使得它們在實現AOP的技術機制上,大體相似,無非是利用靜態織入或動態織入的方式,完成對aspect的實現。
目前在.Net平台下的AOP大部分仍然處於最初的開發階段,各自發布的版本基本都是beta版。其中較有代表性的AOP工具包括Aspect#,Spring.Net,Eos等。
Aspect#是基於Castle動態代理技術實現的。Castle動態代理技術利用了.Net的Emit技術,生成一個新的類去實現特定的接口,或者擴展一個已有的類,並將其委託指向IInterceptor接口的實現類。通過Castle動態代理技術,就可以攔截方法的調用,並將Aspect的業務邏輯織入到方法中。利用Castle動態代理技術,最大的缺陷是它只對虛方法有效,這限制了Aspect#的一部分應用。
Spring.Net從根本意義上來說,是對Spring工具從Java平台向.Net平台的完全移植。它在AOP的實現上與Spring幾乎完全相似,仍然利用了AOP聯盟提供的攔截器、Advice等實現AOP。Spring.Net的配置文件也與Spring相同。
Eos採用的是靜態織入的技術。它提供了獨有的編譯器,同時還擴展了C#語法,以類似於AspectJ的結構,規定了一套完整的AOP語法,諸如aspect,advice,before,after,pointcut等。Eos充分的利用了.Net中元數據的特點,以IL級的代碼對方面進行織入,這也使得它的性能與其他AOP工具比較有較大的提高。
4.2 .Net平台下實現AOP的技術基礎
    如前所述,在.Net平台下實現AOP,採用的方式主要是靜態織入和動態織入的方式。在本文中,我將充分利用.Net的技術特性,包括元數據、Attribute、.Net Remoting的代理技術,將其綜合運用,最終以動態織入的方式實現AOP公共類庫。本節將介紹實現AOP所必需的.Net知識。
4.2.1元數據(metadata)
4.2.1.1元數據概述
元數據是一種二進制信息,用以對存儲在公共語言運行庫(CLR)中可移植可執行文件(PE)或存儲在內存中的程序進行描述。在.Net中,如果將代碼編譯為PE文件時,便會將元數據插入到該文件的一部分中,而該代碼被編譯成的Microsoft中間語言(MSIL),則被插入到該文件的另一部分中。在模塊或程序集中定義和引用的每個類型和成員都將在元數據中進行說明。執行代碼時,運行庫將元數據加載到內存中,並引用它來發現有關代碼的類、成員、繼承等信息。
元數據以非特定語言的方式描述在代碼中定義的每一類型和成員。它存儲的信息包括程序集的信息,如程序集的版本、名稱、區域性和公鑰,以及該程序集所依賴的其他程序集;此外,它還包括類型的說明,包括類型的基類和實現的接口,類型成員(方法、字段、屬性、事件、嵌套的類型)。
在.Net Framework中,元數據是關鍵,該模型不再需要接口定義語言(IDL) 文件、頭文件或任何外部組件引用方法。元數據允許.NET 語言自動以非特定語言的方式對其自身進行描述,此外,通過使用Attribute,可以對元數據進行擴展。元數據具有以下主要優點:
1. 自描述文件
公共語言運行庫(CLR)模塊和程序集是自描述的。模塊的元數據包含與另一個模塊進行交互所需的全部信息。元數據自動提供COM中IDL的功能,允許將一個文件同時用於定義和實現。運行庫模塊和程序集甚至不需要向操作系統註冊。運行庫使用的說明始終反映編譯文件中的實際代碼,從而提高應用程序的可靠性。
2.語言互用性和更簡單的基於組件的設計
元數據提供所有必需的有關已編譯代碼的信息,以供您從用不同語言編寫的PE 文件中繼承類。您可以創建用任何託管語言(任何面向公共語言運行庫的語言)編寫的任何類的實例,而不用擔心顯式封送處理或使用自定義的互用代碼。
3.Attribute
.NET Framework允許在編譯文件中聲明特定種類的元數據(稱為Attribute)。在整個.NET Framework 中到處都可以發現Attribute的存在,Attribute用於更精確地控制運行時程序如何工作。另外,用戶可以通過自定義屬性向.NET Framework 文件發出用戶自己的自定義元數據​​。
4.2.1.2元數據的結構
在PE文件中與元數據有關的主要包括兩部分。一部分是元數據,它包含一系列的表和堆數據結構。每個元數據表都保留有關程序元素的信息。例如,一個元數據表說明代碼中的類,另一個元數據表說明字段等。如果您的代碼中有10個類,類表將有10行,每 ​​行為1個類。元數據表引用其他的表和堆。例如,類的元數據表引用方法表。元數據以四種堆結構存儲信息:字符串、Blob、用戶字符串和GUID。所有用於對類型和成員進行命名的字符串都存儲在字符串堆中。例如,方法表不直接存儲特定方法的名稱,而是指向存儲在字符串堆中的方法的名稱。
另一部分是MSIL指令,許多MSIL指令都帶有元數據標記。元數據標記在PE 文件的MSIL 部分中唯一確定每個元數據表的每一行。元數據標記在概念上和指針相似,永久駐留在MSIL中,引用特定的元數據表。元數據標記是一個四個字節的數字。最高位字節表示特定標記(方法、類型等)引用的元數據表。剩下的三個字節指定與所說明的編程元素對應的元數據表中的行。如果用C#定義一個方法並將其編譯到PE文件中,下面的元數據標記可能存在於PE文件的MSIL部分:
0x06000004
最高位字節(0x06) 表示這是一個MethodDef標記。低位的三個字節(000004) 指示公共語言運行庫在MethodDef 表的第四行查找對該方法定義進行描述的信息。
表4.1 描述了PE文件中元數據的結構及其每部分的內容:
PE 部分
PE 部分的內容
PE 標頭
PE 文件主要部分的索引和入口點的地址。
運行庫使用該信息確定該文件為 PE 文件並確定當將程序加載到內存時執行從何處開始。
MSIL 指令
組成代碼的 Microsoft 中間語言指令 (MSIL) 。許多 MSIL 指令帶有元數據標記。
元數據
元數據表和堆。運行庫使用該部分記錄您的代碼中每個類型和成員的信息。本部分還包括自定義屬性和安全性信息。
表4.1 PE文件中的元數據
4.2.1.3元數據在運行時的作用
由於在MSIL指令中包含了元數據標記,因此,當公共語言運行庫(CLR)將代碼加載到內存時,將向元數據諮詢該代碼模塊中包含的信息。運行庫對Microsoft中間語言(MSIL)流執行廣泛的分析,將其轉換為快速本機指令。運行庫根據需要使用實時(JIT)編譯器將MSIL指令轉換為本機代碼,每次轉換一個方法。例如,有一個類APP,其中包含了Main()方法和Add()方法:
using System; 
public class App
{
   public static int Main()
   {
      int ValueOne = 10;
      int ValueTwo = 20;        
      Console.WriteLine("The Value is: {0}", Add(ValueOne, ValueTwo));
      return 0;
   }
   public static int Add(int One, int Two)
   {
      return (One + Two);
   }
}
通過運行庫,這段代碼被加載到內​​存中,並被轉化為MSIL:
.entrypoint
.maxstack 3
.locals ([0] int32 ValueOne,
         [1] int32 ValueTwo,
         [2] int32 V_2,
         [3] int32 V_3)
IL_0000: ldc.i4.s 10
IL_0002: stloc.0
IL_0003: ldc.i4.s 20
IL_0005: stloc.1
IL_0006: ldstr "The Value is: {0}"
IL_000b: ldloc.0
IL_000c: ldloc.1
IL_000d: call int32 ConsoleApplication.MyApp::Add(int32,int32) /* 06000003 */
JIT 編譯器讀取整個方法的MSIL,對其進行徹底地分析,然後為該方法生成有效的本機指令。在IL_000d 遇到Add 方法(/* 06000003 */) 的元數據標記,運行庫使用該標記參考MethodDef 表的第三行。
表4.2顯示了說明Add 方法的元數據標記所引用的MethodDef 表的一部分:
 
相對虛擬地址(RVA)
ImplFlags
Flags
Name
(指向字符串堆)
Signature
(指向 Blob 堆)
1
0x00002050
IL
Managed
Public
ReuseSlot
SpecialName
RTSpecialName
.ctor
.ctor (構造函數)

2
0x00002058
IL
Managed
Public
Static
ReuseSlot
Main
String
3
0x0000208c
IL
Managed
Public
Static
ReuseSlot
Add
int, int, int

表4.2 元數據標記
該表的每一列都包含有關代碼的重要信息。RVA 列允許運行庫計算定義該方法的MSIL 的起始內存地址。ImplFlags 和Flags 列包含說明該方法的位屏蔽(例如,該方法是公共的還是私有的)。Name 列對來自字符串堆的方法的名稱進行了索引。Signature 列對在Blob 堆中的方法簽名的定義進行了索引。
通過利用元數據,我們就可以獲得類的相關信息。如上所述,在類APP的MethodDef表中,可以獲得類APP的三個方法,以及方法的Flags和方法簽名。而在.Net中,則提供了反射技術,來支持這種對元數據信息的獲取。可以說,正是因為有了元數據,才使得AOP的攔截與織入功能的實現成為可能。
4.2.2 Attribute
4.2.2.1 Attribute概述
通過對.Net元數據的分析,我們知道可以通過Attribute來擴展元數據。那麼什麼是Attribute?在MSDN中,Attribute被定義為“是被指定給某一聲明的一則附加的聲明性信息”。我們可以通過Attribute來定義設計層面的信息以及運行時(run-time)信息,也可以利用Attribute建立自描述(self-describing)組件。
Attribute可應用於任何目標元素,我們可以通過AttributeTargets枚舉指定其施加的目標,AttributeTargets枚舉在.Net中的定義如下:
public enum AttributeTargets
{
   All=16383,
   Assembly=1,
   Module=2,
   Class=4,
   Struct=8,
   Enum=16,
   Constructor=32,
   Method=64,
   Property=128,
   Field=256,
   Event=512,
   Interface=1024,
   Parameter=2048,
   Delegate=4096,
   ReturnValue=8192
}
作為參數的AttributeTarges的值允許通過“或”操作來進行多個值的組合,如果你沒有指定參數,那麼默認參數就是All 。
不管是.Net Framework提供的Attribute,還是用戶自定義Attribute,都是通過[]施加到目標元素上。雖然Attribute的用法與通常的類型不一樣,但在.Net內部,Attribute本質上還是一個類。但是,Attribute類的實例化發生在編譯時,而非運行時,因而達到了擴展元數據的目的。一個Attribute的多個實例可應用於同一個目標元素;並且Attribute可由從目標元素派生的元素繼承。
4.2.2.2自定義Attribute
.Net Framework支持用戶自定義Attribute。自定義Attribute的方法與定義類一樣,唯一不同之處是自定義的Attribute必須繼承Attribute類。Attribute類包含用於訪問和測試自定義Attribute的簡便方法。其中,Attribute類的構造函數為protected,只能被Attribute的派生類調用。Attribute類包含的方法主要為:
1.三個靜態方法
static Attribute GetCustomAttribute():這個方法有8種重載的版本,它被用來取出施加在類成員上指定類型的Attribute。
static Attribute[] GetCustomAttributes(): 這個方法有16種重載版本,用來取出施加在類成員上指定類型的Attribute數組。
static bool IsDefined():有八種重載版本,看是否指定類型的定制attribute被施加到類的成員上面。
2.兩個實例方法
bool IsDefaultAttribute(): 如果Attribute的值是默認的值,那麼返回true。
bool Match():表明這個Attribute實例是否等於一個指定的對象。
3.公共屬性
TypeId: 得到一個唯一的標識,這個標識被用來區分同一個Attribute的不同實例。
通過自定義Attribute,可使得用戶自定義的信息與Attribute施加的類本身相關聯。例如,給定一個自定義的.NET 屬性,我們就可以輕鬆地將調用跟踪Attribute與類的方法相關聯:
public class Bar
{
    [CallTracingAttribute("In Bar ctor")]
    public Bar() {}
    [CallTracingAttribute("In Bar.Calculate method")]
    public int Calculate(int x, int y){ return x + y; }
}
請注意,方括號中包含CallTracingAttribute 和訪問方法時輸出的字符串。這是將自定義元數據​​與Bar 的兩個方法相關聯的Attribute語法。該自定義的Attribute實現,如下所示:
using System;
using System.Reflection;
[AttributeUsage( AttributeTargets.ClassMembers, AllowMultiple = false )]
public class CallTracingAttribute : Attribute
{    
    private string m_TracingInfo;
    public CallTracingAttribute(string info)
{
           m_TracingInfo = info;
    }
    public string TracingInfo
    {
        get {return tracingInfo;}
    }
}
通過自定義的CallTracingAttribute,將一段Tracing信息施加到類Bar的構造函數和方法Calculate上。我們可以利用反射技術與Attribute類提供的方法,來獲得Bar類的元數據中包含的Attribute信息,如:
public class Test
{
       public static void Main(string[] args)
       {
           System.Reflection.MemberInfo info = typeof(Bar);
CallTracingAttribute attribute = (CallTracingAttribute) Attribute.GetCustomAttribute(info,typeof(CallTracingAttribute));
              if (attribute != null)
              {
                     Console.WriteLine(“Tracing Information:{0}”,attribute.TracingInfo);
              }
}
}
4.2.2.3上下文(Context)和Attribute
所謂上下文(Context),是指一個邏輯上的執行環境。每一個應用程序域都有一個或多個Context,.Net中的所有對像都會在相應的Context中創建和運行。如圖4.1所示,它顯示了一個安全地存在於Context的對象:
aop4.1.gif

圖4.1 安全地存在於Context的對象
在圖4.1中,上下文(Context)提供了錯誤傳播、事務管理和同步功能,而對象的創建和運行就存在於該Context中。在.Net中,提供了ContextBoundObject類,它代表的含義就是該對象應存在於指定的Context邊界中(Object that will be bound with a context)。凡是繼承了ContextBoundObject類的類類型,就自動具備了對象與Context之間的關係。事實上,如果一個類對像沒有繼承自ContextBoundObject,則該對象默認會創建和運行在應用程序域的default context中,而繼承自ContextBoundObject的類對象,在其對象實例被激活時,CLR將自動創建一個單獨的Context供其生存。
如果需要判定ContextBoundObject類型對象所認定的Context,只需要為該類型對象施加ContextAttribute即可。ContextAttribute類繼承了Attribute類,它是一個特殊的Attribute,通過它,可以獲得對象需要的合適的執行環境,即Context(上下文)。同時,ContextAttribute還實現了IContextAttribute和IContextProperty接口。
由於在施加Attribute時,只需要獲取ContextBoundObject類型的Context屬性,因此,我們也可以自定義Attribute,只需要該自定義的Attribute實現IContextAttribute即可。IContextAttribute接口的定義如下:
public interface IContextAttribute
{
    bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg);
    void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg);
}
每個context attribute在context的構造階段(通常是由ContextBoundObject對象構造動作引發的)會被首先問到IsContextOK,就是說新創建的這個ContextBoundObjec(通過ctorMsg可以知道是哪個對象的哪個構造方法被用來構造ContextBoundObjec對象的)能不能在給定的ctx中存在?這個目的主要是減少應用程序域中潛在的context的數量,如果某些ContextBoundObjec類型可以共用一個有所需特性的執行環境的話,就可以不用再創建新的環境,而只要在已有的環境中​​構造並執行就好了。
如果ContextBoundObjec類型上設置的所有context attributes都認同給定的context(也即調用代碼所處的context)是正確地的(此時IsContextOK均返回true),那麼新的ContextBoundObjec就會被綁定到這個context上。否則,只有有一個attribute返回false,就會立即創建一個新的context。然後,CLR會再一次詢問每一個context attribute新構造的context是否正確,由於Context已經被重新創建,通常此時返回的結果應為false。那麼,Context構造程序就會調用其GetPropertiesForNewContext()方法,context attribute可以用這個方法傳入的構造器方法調用信息(ctorMsg)中的context properties列表(ContextProperties)來為新建的context增加所需的context properties 。
從AOP的角度來看,Context類似於前面分析的橫切關注點,那麼利用我們自定義的Context Attribute,就可以獲得對象它所存在的上下文,從而建立業務對象與橫切關注點之間的關係。
4.2.3代理(Proxy)
     在程序設計中使用代理(Proxy),最重要的目的是可以通過利用代理對象,實現代理所指向的真實對象的訪問。在GOF的《設計模式》中,將代理(Proxy)模式分為四種:
1、遠程代理(Remote Proxy)。它為一個位於不同的地址空間的對象提供一個局域代表對象。這個不同的地址空間可以是在本機器中,亦可是在另一台機器中。
2、虛代理(Virtual Proxy)。它能夠根據需要創建一個資源消耗較大的對象,使得此對像只在需要時才會被真正創建。
3、保護代理(Protection Proxy)。它控制對原始對象的訪問,如果需要可以給不同的用戶提供不同級別的使用權限。
4、智能引用代理(Smart Reference Proxy)。它取代了簡單的指針,在訪問一個對象時,提供一些額外的操作。例如,對指向實際對象的引用計數,這樣當該對像沒有引用時,可以自動釋放它。當第一次引用一個持久對象時,智能引用可以將該對象裝入內存。在訪問一個實際對象前,檢查該對像是否被鎖定,以確保其他對像不能改變它。
在.Net Remoting中,採用了遠程代理(Remote Proxy)模式。採用代理技術,使得對象可以在兩個不同的應用程序域(甚至可以是兩台不同的機器)之間傳遞。代理在.Net中被分為透明代理(Transparent Proxy)和真實代理(Real Proxy)。Transparent Proxy的目標是在CLR 中在IL 層面最大程度扮演被代理的遠端對象,從類型轉換到類型獲取,從字段訪問到方法調用。對CLR 的使用者來說,Transparent Proxy和被其代理的對象完全沒有任何區別,只有通過RemotingServices.IsTransparentProxy 才能區分兩者的區別。Real Proxy則是提供給CLR 使用者擴展代理機制的切入點,通過從Real Proxy繼承並實現Invoke 方法,用戶自定義代理實現可以自由的處理已經被從棧調用轉換為消息調用的目標對象方法調用,如實現緩存、身份驗證、安全檢測、延遲加載等等。
如果我們希望自己定義的代理類能夠“模仿”真實對象的能力,首先就需要實現透明代理。然而,CLR中雖然提供了這樣一個透明代理類(_TransparentProxy),我們卻不能讓自己的代理類從透明代理類派生,也不能通過自定義Attribute、實現標誌性接口等方式將代理類標識為透明代理,從而讓CLR能夠認識。要獲取透明代理,必須要提供一個真實代理。一個真實代理是一個從System.Runtime.Remoting.Proxies.RealProxy派生而來的類。這個RealProxy類的首要功能就是幫我們在運行期動態生成一個可以透明兼容於某一個指定類的透明代理類實例。從RealProxy的源代碼,可以看出透明代理和真實代理之間的關係:
namespace System.Runtime.Remoting.Proxies
{
  abstract public class RealProxy
  {
    protected RealProxy(Type classToProxy) : this(classToProxy, (IntPtr)0, null)
    {
    }
    protected RealProxy(Type classToProxy, IntPtr stub, Object stubData)
    {
      if(!classToProxy.IsMarshalByRef && !classToProxy.IsInterface)
        throw new ArgumentException(...);

      if((IntPtr)0 == stub)
      {
        stub = _defaultStub;
        stubData = _defaultStubData;
      }
      _tp = null;
      if (stubData == null)
        throw new ArgumentNullException("stubdata");
      _tp = RemotingServices.CreateTransparentProxy(this, classToProxy, stub, stubData);
    }
    public virtual Object GetTransparentProxy()
    {
      return _tp;
    }
  }
}
很明顯,透明代理(Transparent Proxy)是在RealProxy類的構造函數中,調用RemotingServices.CreateTransparentProxy()方法動態創建的。CreateTransparentProxy()方法將把被代理的類型強制轉換為統一的由CLR 在運行時創建的RuntimeType 類型,進而調用Internal 方法完成TransparentProxy的創建。通過GetTransparentProxy()方法,就可以獲得創建的這個透明代理對象。因此,要定義自己的真實代理對象,只需要繼承RealProxy類即可:
using System.Runtime.Remoting.Proxies;
public class MyRealProxy: RealProxy
{
  public MyRealProxy(Type classToProxy): base(classToProxy)
  {
    …
  }
}

透明代理和真實代理在上下文(Context)中,會起到一個偵聽器的作用。首先,透明代理將調用堆棧序列化為一個稱為消息(Message)的對象,然後再將消息傳遞給真實代理。真實代理接收消息,並將其發送給第一個消息接收進行處理。第一個消息接收對消息進行預處理,將其繼續發送給位於客戶端和對象之間的消息接收堆棧中的下一個消息接收,然後對消息進行後處理。下一個消息接收也如此照辦,以此類推,直到到達堆棧構建器接收,它將消息反序列化回調用堆棧,調用對象,序列化出站參數和返回值,並返回到前面的消息接收。這個調用鏈如圖4.2所示。
aop4.2.gif
圖4.2 代理(Proxy)偵聽消息的順序
由於透明代理完全等同於其代理的對象,因此,當我們偵聽到代理對像被調用的消息時,就可以截取該消息,並織入需要執行的方面邏輯,完成橫切關注邏輯與核心邏輯的動態代碼織入。
4.3 .Net平台下AOP技術實現
4.3.1實現原理
     根據對.Net中元數據(Metadata)、Attribute、上下文(Context)、代理(Proxy)等技術要素的分析,要在.Net中實現AOP,首先需要獲得一個類對象的上下文(Context),則其前提就是這個類必須從System.ContextBoundObject類派生。這個類對象就相當於AOP中的核心關注點,而類對象的上下文則屬於AOP的橫切關注點。很顯然,只需要利用上下文,就可以方便的實現核心關注點和橫切關注點的分離。
正如圖4.1所示,對像是存在於上下文中的。利用自定義Attribute,可以建立對象與上下文之間的關聯。Attribute可以擴展對象的元數據,從而標識出該對象屬於其中的一個或多個Aspect。一旦該對象實例被創建或調用時,就可以利用反射技術獲得該對象的自定義Attribute。為使得對象的元數據與上下文關聯起來,就要求這個自定義的Attribute必須實現接口IContextAttribute。
獲得了對象的上下文之後,透明代理與真實代理就能夠對該對象的方法調用(包括構造函數)進行偵聽,並完成消息的傳遞。傳遞的消息可以被Aspect截取,同時利用真實代理,也可以完成對業務對象的Decorate,將Aspect邏輯註入到業務對像中。由於在大型的企業系統設計中,橫切關注點會包括事務管理、日誌管理、權限控制等多方面,但由於方面(Aspect)在技術上的共同特性,我們可以利用.Net的相關技術實現方面(Aspect)的核心類庫,所有的橫切關注點邏輯,都可以定義為派生這些類庫的類型,從而真正在.Net中實現AOP技術。
4.3.2 AOP公共類庫
4.3.2.1 AOP Attribute
如上所述,要實現AOP技術,首先需要自定義一個Attribute。該自定義Attribute必須實現IContextAttribute,因此其定義如下所示:
using System;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Activati​​on;
[AttributeUsage(AttributeTargets.Class)]
       public abstract class AOPAttribute:Attribute,IContextAttribute
       {
private string m_AspectXml;
       private const string CONFIGFILE = @"configuration\aspect.xml";
              public AOPAttribute()                    
              {
m_AspectXml = CONFIGFILE;
        }   
public AOPAttribute(string aspectXml)
        {
            this.m_AspectXml = aspectXml;
        }  
               protected abstract AOPProperty GetAOPProperty();
        #region IContextAttribute Members
        public sealed void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
        {
             AOPProperty property = GetAOPProperty();    
                     property.AspectXml = m_AspectXml;      
           ctorMsg.ContextProperties.Add(property);
        }
        public bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
        {
            return false;
        }
}
類AOPAttribute除了繼承System.Attribute類之外,關鍵之​​處在於實現了接口IContextAttribute接口。接口方法GetPropertiesForNewContext()其功能是向Context添加屬性(Property)集合,這個集合是IConstructionCallMessage對象的ContextProperties屬性。而接口方法IsContextOK(),則用於判斷Context中是否存在指定的屬性。這個方法會在Context的構造階段(通常是由被施加了AOPAttribute的業務對像在創建時引發的)被調用,如果返回false,會創建一個新的Context。
GetAOPProperty()方法是一個受保護的抽象方法,繼承AOPAttribute的子類將重寫該方法,返回一個AOPProperty對象。在這裡,我們利用了Template Method模式,通過該方法創建符合條件的AOPProperty對象,並被GetPropertiesForNewContext()方法添加到屬性集合中。
抽像類AOPAttribute是所有與方面有關的Attribute的公共基類。所有方面的相關Attribute均繼承自它,同時實現GetAOPProperty()方法,創建並返回與之對應的AOPProperty對象。
4.3.2.2 AOP Property
ContextProperties是一個特殊的集合對象,它存放的是對像被稱為Context Property,是一個實現了IContextProperty接口的對象,這個對象可以為相關的Context提供一些屬性。IContextProperty接口的定義如下:
public interface IContextProperty
{
    string Name { get; }
    bool IsNewContextOK(Context newCtx);
    void Freeze(Context newCtx);
}
IContextProperty接口的Name屬性,表示Context Property的名字,Name屬性值要求在整個Context中必須是唯一的。IsNewContextOK()方法用於確認Context是否存在衝突的情況。而Freeze()方法則是通知Context Property,當新的Context構造完成時,則進入Freeze狀態(通常情況下,Freeze方法僅提供一個空的實現)。
由於IContextProperty接口僅僅是為Context提供一些基本信息,它並不能完成對方法調用消息的截取。根據對代理技術的分析,要實現AOP,必須在方法調用截取消息傳遞,並形成一個消息鏈Message Sink。因此,如果需要向所在的Context的Transparent Proxy/Real Proxy中植入Message Sink,Context Property還需要提供Sink的功能。所幸的是,.Net已經提供了實現MessageSink功能的相關接口,這些接口的命名規則為IContributeXXXSink,XXX代表了四種不同的Sink:Envoy,ClientContext,ServerContext,Object。這四種接口有其相似之處,都只具有一個方法用於返回一個IMessageSink對象。由於我們需要獲取的透明代理對象,是能夠穿越不同的應用程序域的。在一個應用程序域收到其他應用程序域的對象,則該對像在.Net中被稱為Server Object,該對象所處的Context也被稱為Server Context。我們在.Net中實現AOP,其本質正是要獲得對象的Server Context,並截取該Context中的方法調用消息,因而Context Property對象應該實現IContributeServerContextSink接口。事實上,也只有IContributeServerContextSink接口的GetServerContextSink()方法,才能攔​​截包括構造函數在內的所有方法的調用。
因此,AOP Property最終的定義如下:
using System;
using System.Runtime.Remoting.Activati​​on;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Messaging;
public abstract class AOPProperty : IContextProperty, IContributeServerContextSink
    {
private string m_AspectXml;
        public AOPProperty()
        {
                     m_AspectXml = string.Empty;          
        }
public string AspectXml
        {
            set { m_AspectXml = value; }
        }
        protected abstract IMessageSink CreateAspect(IMessageSink nextSink);
        protected virtual string GetName()
        {
            return "AOP";
        }
        protected virtual void FreezeImpl(Context newContext)
        {
            return;
        }
        protected virtual bool CheckNewContext(Context newCtx)
        {
            return true;
        }
        #region IContextProperty Members
        public void Freeze(Context newContext)
        {
            FreezeImpl(newContext);
        }
        public bool IsNewContextOK(Context newCtx)
        {
            return CheckNewContext(newCtx);
        }
        public string Name
        {
            get { return GetName(); }
        }
        #endregion
        #region IContributeServerContextSink Members
        public IMessageSink GetServerContextSink(IMessageSink nextSink)
        {
            Aspect aspect = (Aspect)CreateAspect(nextSink);           
            aspect.ReadAspect(m_AspectXml,Name);           
            return (IMessageSink)aspect;
        }
        #endregion
    }
在抽像類AOPProperty中,同樣利用了Template Method模式,將接口IContextProperty的方法的實現利用受保護的虛方法延遲到繼承AOPProperty的子類中。同時,對於接口IContributeServerContextSink方法GetServerContextSink(),則創建並返回了一個Aspect類型的對象,Aspect類型實現了IMessageSink接口,它即為AOP中的方面,是所有方面(Aspect)的公共基類。
AOPProperty類作為抽像類,是所有與上下文有關的Property的公共基類。作為Context Property應與Aspect相對應,且具體的AOPProperty類對象應在AOPAttribute的子類中創建並獲得。
4.3.2.3 Aspect與PointCut
Aspect類是AOP的核心,它的本質是一個Message Sink,代理正是通過它進行消息的傳遞,並截獲方法間傳遞的消息。Aspect類實現了IMessageSink接口,其定義如下:
public interface IMessageSink
{
IMessage SyncProcessMessage(IMessage msg);
IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink);    
    IMessageSink NextSink { get; }       
}
IMessageSink接口利用NextSink將多個Message Sink連接起來,以形成一個消息接收器鏈;而SyncProcessMessage()和AsyncProcessMessage()方法則分別用於同步和異步操作,它們在消息傳遞的時候被調用。
注意方法SyncProcessMessage()中的參數,是一個IMessage接口類型的對象。在.Net中,IMet​​hodCallMessage和IMethodReturnMessage接口均繼承自IMessage接口,前者是調用方法的消息,而後者則是方法被調用後的返回消息。利用這兩個接口對象,就可以獲得一個對象方法的切入點。因此,一個最簡單的Aspect實現應該如下:
public abstract class Aspect : IMessageSink
    {       
        private IMessageSink m_NextSink;

        public AOPSink(IMessageSink nextSink)
        {
            m_NextSink = nextSink;          
        }
        public IMessageSink NextSink
        {
            get { return m_NextSink; }
        }
        public IMessage SyncProcessMessage(IMessage msg)
        {
            IMethodCallMessage call = msg as IMethodCallMessage;
            if (call == null)
{
                              return null;
}
IMessage retMsg = null;
BeforeProcess();
retMsg = m_NextSink.SyncProcessMessage(msg);
AfterProcess();
                return retMsg;
        }
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
          {
             return null;
          }
          private void BeforeProcess()
          {
                     //方法調用前的實現邏輯;
              }
private void AfterProcess()
          {
                     //方法調用後的實現邏輯;
              }
}
注意在方法SyncProcessMessage()中,IMessageSink對象m_NextSink通過Aspect構造函數賦值為業務對象的透明代理,在調用m_NextSink的SyncProcessMessage()方法時,此時調用的是與該透明代理對應的真實代理。如果在消息接收鏈中還存在代理,在方法調用將會沿著消息鏈不斷的向後執行。而對於一個業務對象而言,此時的IMessage即為該對像中被調用的方法,SyncProcessMessage(msg)就相當於執行了該方法。而在m_NextSink.SyncProcessMessage(msg)方法前後執行BeforeProcess()和AfterProcess(),就完成了對方法的截取,並將自己的Aspect邏輯織入到業務對象的方法調用中,從而實現了AOP。
然而對於AOP技術的實際應用而言,並非業務對象的所有方法都需要被截取進而進行方面的織入。也即是說,切入點(PointCut)必須實現可被用戶定義。而所謂切入點,實際上是業務對象方法與Advice之間的映射關係。在.Net中,我們可以通過集合對象來管理這個映射關係。由於Advice包括Before Advice和After Advice,因此,在Aspect類中應該定義兩個集合對象:
private SortedList m_BeforeAdvices;
private SortedList m_AfterAdvices;
在添加PointCut時,是將方法名和具體的Advice對象建立映射,根據SortedList集合的特性,我們將方法名作為SortedList的Key,而Advice則作為SortedList的Value:
protected virtual void AddBeforeAdvice(string methodName, IBeforeAdvice before)
        {
            lock (this.m_BeforeAdvices)
            {
                if (!m_BeforeAdvices.Contains(methodName))
                {
                    m_BeforeAdvices.Add(methodName, before);
                }
            }
        }
        protected virtual void AddAfterAdvice(string methodName, IAfterAdvice after)
        {
            lock (this.m_AfterAdvices)
            {
                if (!m_AfterAdvices.Contains(methodName))
                {
                    m_AfterAdvices.Add(methodName, after);
                }
            }
        }
在向SortedList添加PointCut時,需要先判斷集合中是否已經存在該PointCut。同時考慮到可能存在並發處理的情況,在添加PointCut時,利用lock對該操作進行了加鎖,避免並發處理時可能會出現的錯誤。
建立了方法名和Advice的映射關係,在執行SyncProcessMessage()方法,就可以根據IMessage的值,獲得業務對像被調用方法的相關屬性,然後根據方法名,找到其對應的Advice,從而執行相關的Advice代碼:
public IMessage SyncProcessMessage(IMessage msg)
              {           
                     IMethodCallMessage call = msg as IMethodCallMessage;
                     string methodName = call.MethodName.ToUpper();
                     IBeforeAdvice before = FindBeforeAdvice(methodName);
                     if (before != null)
                     {
                            before.BeforeAdvice(call);
                     }           
                     IMessage retMsg = m_NextSink.SyncProcessMessage(msg);
                     IMethodReturnMessage reply = retMsg as IMethodReturnMessage;
            IAfterAdvice after = FindAfterAdvice(methodName);
                     if (after != null)
                     {
                            after.AfterAdvice(reply);
                     }
                     return retMsg;
              }
其中FindBeforeAdvice()和FindAfterAdvice()方法完成key和value的查找工作,分別的定義如下:
public IBeforeAdvice FindBeforeAdvice(string methodName)
        {
            IBeforeAdvice before;
            lock (this.m_BeforeAdvices)
            {
                before = (IBeforeAdvice)m_BeforeAdvices[methodName];
            }
            return before;
        }
        public IAfterAdvice FindAfterAdvice(string methodName)
        {
            IAfterAdvice after;
            lock (this.m_AfterAdvices)
            {
                after = (IAfterAdvice)m_AfterAdvices[methodName];
            }
            return after;
        }
在找到對應的Advice對像後,就可以調用Advice對象的相關方法,完成方面邏輯代碼的織入。
那麼,PointCut是在什麼時候添加的呢?我們可以在AOP的配置文件(Aspect.xml)中配置PointCut,然後在Aspect類中,通過ReadAspect()方法,讀入配置文件,獲取PointCut以及Aspect需要的信息,包括方法名和Advice對象(通過反射動態創建),在執行AddBeforeAdvice()和AddAfterAdvice()方法將PointCut添加到各自的集合對像中:
public void ReadAspect(string aspectXml,string aspectName)
{
    IBeforeAdvice before = (IBeforeAdvice)Configuration.GetAdvice(aspectXml,aspectName,Advice.Before);
    string[] methodNames = Configuration.GetNames(aspectXml,aspectName,Advice.Before);
    foreach (string name in methodNames)
    {
              AddBeforeAdvice(name,before);
       }
IAfterAdvice after = (IAfterAdvice)Configuration.GetAdvice(aspectXml,aspectName,Advice.After);
    string[] methodNames = Configuration.GetNames(aspectXml,aspectName,Advice.After);
    foreach (string name in methodNames)
    {
              AddAfterAdvice(name,after);
       }   
}
一個Aspect的配置文件示例如下:
                     ADD
                     SUBSTRACT
                     ADD
                     SUBSTRACT
配置文件中,元素Advice的assembly屬性和class屬性值,是利用反射創建Advice對象所需要的信息。另外,Aspect的名字應與方面的Property名保持一致,因為ReadAspect()方法是通過AOPProperty名字來定位配置文件的Aspect。
4.3.2.4 Advice
在Aspect類中,已經使用了Advice對象。根據類別不同,這些Advice對象分別實現IBeforeAdvice接口和IAfterAdvice接口:
using System;
using System.Runtime.Remoting.Messaging;
public interface IBeforeAdvice
    {
        void BeforeAdvice(IMethodCallMessage callMsg);
    }
public interface IAfterAdvice
    {
        void AfterAdvice(IMethodReturnMessage returnMsg);
    }
接口方法應該實現具體的方面邏輯,同時可以通過IMethodCallMessage對象獲得業務對象的調用方法信息,通過IMethodReturnMessage對象獲得方法的返回信息。
4.4 .Net平台AOP技術應用案例
在4.3.2節,我們已基本實現了AOP的公共類庫,這其中包括AOPAttribute,AOPProperty,Aspect,IBeforeAdvice,IAfterAdvice。根據這些公共基類或接口,我們就可以定義具體的方面,分別繼承或實現這些類與接口。為了展示AOP在.Net中的應用,在本節,我將以一個簡單的實例來說明。
假定我們要設計一個計算器,它能提供加法和減法功能。我們希望,在計算過程中,能夠通過日誌記錄整個計算過程及其結果,同時需要監測其運算性能。該例中,核心業務是加法和減法,而公共的業務則是日誌與監測功能。根據前面對AOP的分析,這兩個功能作為橫切關注點,將是整個系統需要剝離出來的“方面”。
4.4.1日誌方面
     作為日誌方面,其功能就是要截取業務對象方法的調用,並獲取之間傳遞的消息內容。從上節的分析我們知道,方法間的消息可以從IMethodCallMessage和IMethodReturnMessage接口對象獲得。因此,實現日誌方面,最重要的是實現Aspect類中的SyncProcessMessage()方法。此外,也應定義與之對應的Attribute和Property,以及實現日誌邏輯的Advice。
4.4.1.1日誌Attribute(LogAOPAttribute)
LogAOPAttribute類繼承AOPAttribute,由於AOPAttribute類主要是創建並獲得對應的AOPProperty,因此,其子類也僅需要重寫父類的受保護抽象方法GetAOPProperty()即可:
[AttributeUsage(AttributeTargets.Class)]
       public class LogAOPAttribute:AOPAttribute
       {
              public LogAOPAttribute():base()
              {
              }
              public LogAOPAttribute(string aspectXml):base(aspectXml)
              {
              }
              protected override AOPProperty GetAOPProperty()
              {
                     return new LogAOPProperty();
              }    
       }
通過對GetAOPProperty()方法的重寫,創建並獲得了與LogAOPAttribute類相對應的LogAOPProperty,此時在LogAOPAttribute所施加的業務對象的上下文中,所存在的AOP Property就應該是具體的LogAOPProperty對象。
4.4.1.2日誌Property(LogAOPProperty)
由於Context Property的名字在上下文中必須是唯一的,因此每個方面的Property的名字也必須是唯一的。因此在繼承AOPProperty的子類LogAOPProperty中,必須重寫父類的虛方法GetName(),同時在LogAOPProperty中,還應該創建與之對應的Aspect,也即是Message Sink,而這個工作是由抽象方法CreateAspect ()來完成的。因此,LogAOPProperty類的定義如下:
       public class LogAOPProperty:AOPProperty
       {
              protected override IMessageSink CreateAspect(IMessageSink nextSink)
              {
                     return new LogAspect(nextSink);
              }

              protected override string GetName()
              {
                     return "LogAOP";
              }
       }
為避免Property的名字出現重複,約定成俗以方面的Attribute名為Property的名字,以本例而言,其Property名為LogAOP。
4.4.1.3日誌Aspect(LogAspect)
LogAspect完成的功能主要是將Advice與業務對象的方法建立映射,並將其添加到Advice集合中。由於我們在AOP實現中,利用了xml配置文件來配置PointCut,因此對於所有Aspect而言,這些操作都是相同的,只要定義了正確的配置文件,將其讀入即可。對於Aspect的SyncProcessMessage(),由於攔截和織入的方法是一樣的,不同的只是Advice的邏輯而已,因此在所有Aspect的公共基類中已經提供了默認的實現:
       public class LogAspect:Aspect
       {
              public LogAspect(IMessageSink nextSink):base(nextSink)
              {                  
              }           
       }
然後定義正確的配置文件:
                     ADD
                     SUBSTRACT
                     ADD
                     SUBSTRACT
    LogAdvice所屬的程序集文件為AOP.Advice.dll,完整的類名為AOP.Advice.LogAdvice。
4.4.1.4日誌Advice(LogAdvice)
由於日誌方面需要記錄方法調用前後的相關數據,因此LogAdvice應同時實現IBeforeAdvice和IAfterAdvice接口:
public class LogAdvice:IAfterAdvice,IBeforeAdvice
    {
        #region IBeforeAdvice Members
        public void BeforeAdvice(IMethodCallMessage callMsg)
        {
            if (callMsg == null)
            {
                return;
            }
            Console.WriteLine("{0}({1},{2})", callMsg.MethodName, callMsg.GetArg(0), callMsg.GetArg(1));
        }
        #endregion
        #region IAfterAdvice Members
        public void AfterAdvice(IMethodReturnMessage returnMsg)
        {
            if (returnMsg == null)
            {
                return;
            }
            Console.WriteLine("Result is {0}", returnMsg.ReturnValue);
        }
        #endregion
    }
在BeforeAdvice()方法中,消息類型為IMethodCallMessage,通過這個接口對象,可以獲取方法名和方法調用的參數值。與之相反,AfterAdvice()方法中的消息類型為IMethodReturnMessage,Advice所要獲得的數據為方法的返回值ReturnValue。
4.4.2性能監測方面
     性能監測方面與日誌方面的實現大致相同,為簡便起見,我要實現的性能監測僅僅是記錄方法調用前和調用後的時間。
4.4.2.1性能監測Attribute(MonitorAOPAttribute)
與日誌Attribute相同,MonitorAOPAttribute僅僅需要創建並返回對應的MonitorAOPProperty對象:
[AttributeUsage(AttributeTargets.Class)]
public class MonitorAOPAttribute:AOPAttribute
{
             public MonitorAOPAttribute():base()
             {
             }
             public MonitorAOPAttribute(string aspectXml):base(aspectXml)
             {
             }
             protected override AOPProperty GetAOPProperty()
             {
                    return new MonitorAOPProperty();
             }    
}
4.4.2.2性能監測Property(MonitorAOPProperty)
MonitorAOPProperty的屬性名將定義為MonitorAOP,使其與日誌方面的屬性區別。除定義性能監測方面的屬性名外,還需要重寫CreateAspect()方法,創建並返回對應的方面對象MonitorAspect:
public class MonitorAOPProperty:AOPProperty
      {
             protected override IMessageSink CreateAspect(IMessageSink nextSink)
             {
                    return new MonitorAspect(nextSink);
             }

             protected override string GetName()
             {
                    return "MonitorAOP";
             }
      }
4.4.2.3性能監測Aspect(MonitorAspect)
MonitorAspect類的實現同樣簡單:
    public class MonitorAspect:Aspect
       {
              public MonitorAspect(IMessageSink nextSink):base(nextSink)
              {                  
              }
    }
    而其配置文件的定義則如下所示:
                     ADD
                     SUBSTRACT
                     ADD
                     SUBSTRACT
MonitorAdvice所屬的程序集文件為AOP.Advice.dll,完整的類名為AOP.Advice.MonitorAdvice。
4.4.2.4性能監測Advice(MonitorAdvice)
由於性能監測方面需要記錄方法調用前後的具體時間,因此MonitorAdvice應同時實現IBeforeAdvice和IAfterAdvice接口:
public class MonitorAdvice : IBeforeAdvice, IAfterAdvice
    {
        #region IBeforeAdvice Members
        public void BeforeAdvice(IMethodCallMessage callMsg)
        {
            if (callMsg == null)
            {
                return;
            }
            Console.WriteLine("Before {0} at {1}", callMsg.MethodName, DateTime.Now);
        }
        #endregion

        #region IAfterAdvice Members
        public void AfterAdvice(IMethodReturnMessage returnMsg)
        {
            if (returnMsg == null)
            {
                return;
            }
            Console.WriteLine("After {0} at {1}", returnMsg.MethodName, DateTime.Now);
        }
        #endregion
    }
MonitorAdvice只需要記錄方法調用前後的時間,因此只需要分別在BeforeAdvice()和AfterAdvice()方法中,記錄當前的時間即可。
4.4.3業務對象與應用程序
4.4.3.1業務對象(Calculator)
通過AOP技術,我們已經將核心關注點和橫切關注點完全分離,我們在定義業務對象時,並不需要關注包括日 ​​誌、性能監測等方面,這也是AOP技術的優勢。當然,由於要利用.Net中的Attribute及代理技術,對於施加了方面的業務對象而言,仍然需要一些小小的限制。
首先,我們應該將定義好的方面Aspect施加給業務對象。其次,由於代理技術要獲取業務對象的上下文(Context),該上下文必須是指定的,而非默認的上下文。上下文的獲得,是在業務對象創建和調用的時候,如果要獲取指定的上下文,在.Net中,要求業務對象必須繼承ContextBoundObject類。因此,最後業務對象Calculator類的定義如下所示:
[MonitorAOP]
    [LogAOP]
    public class Calculator : ContextBoundObject
       {
              public int Add(int x,int y)
              {
                     return x + y;
              }
              public int Substract(int x,int y)
              {
                     return x - y;
              }
       }
[MonitorAOP]和[LogAOP]正是之前定義的方面Attribute,此外Calculator類繼承了ContextBoundObject。除此之外,Calculator類的定義與普通的對象定義無異。然而,正是利用AOP技術,就可以攔截Calculator類的Add()和Substract()方法,對其進行日誌記錄和性能監測。而實現日誌記錄和性能監測的邏輯代碼,則完全與Calculator類的Add()和Substract()方法分開,實現了兩者之間依賴的解除,有利於模塊的重用和擴展。
4.4.3.2應用程序(Program)
我們可以實現簡單的應用程序,來看看業務對象Calculator施加了日誌方面和性能檢測方面的效果:
class Program
      {           
             [STAThread]
             static void Main(string[] args)
             {
                    Calculator cal = new Calculator();
                    cal.Add(3,5);
                    cal.Substract(3,5);
                    Console.ReadLine();
             }
      }
程序創建了一個Calculator對象,同時調用了Add()和Substract()方法。由於Calculator對像被施加了日誌方面和性能檢測方面,因此運行結果會將方法調用的詳細信息和調用前後的運行當前時間打印出來,如圖4.3所示:
aop4.3.gif

圖4.3 施加了方面的業務對象調用結果
如果要改變記錄日誌和性能監測結果的方式,例如將其寫到文件中,則只需要改變LogAdvice和MonitorAdvice的實現,對於Calculator對象而言,則不需要作任何改變。