本篇目錄
Hi,guys!Long time no see! 😃
本節的源碼本人已托管於Coding上:點擊查看。
本系列的實驗環境:VS 2017。
讀完本章后,可能仍然不能實現自己的AOP工具,但應該對兩種主要類型(PostSharp和Castle DynamicProxy)的AOP工具的運行原理有了基本的理解。PostSharp是一個在編譯時編織的后期編譯器,Castle DynamicProxy會在運行時生成一個代理類。雖然前面已經說了很多如何使用這些工具,但是在項目中如果用的AOP工具越多,那么准確地理解它們是如何運行的就越重要。本章的目的,充分理解編譯時編織的代表PostSharp和運行時編織的代表DynamicProxy。這些工具都是一流的代表,它們的實現會讓我們明白AOP是如何運行的。
AOP是如何跑起來的
先來回顧下第1章的圖:
在第1章中使用這張圖的目的是說明AOP能夠將橫切關注點划分到單獨的類中,因而與業務邏輯分離並實現了自我封裝。所以,作為一個開發者,不必處理相互交織在一起的代碼,只需要把它交給AOP工具的切面就可以了。你作為一個開發者,只需要讀、寫和維護分離的類,然而,需要明白,這些代碼跑起來時還是會按照交織到一起的代碼進行運行。
直到目前,我們只涉及了這張圖的上半部分,這節開始我們談談下半部分。編織(或交織,Weaving)是AOP框架將分離的類結合在一起的過程。在類被使用前,編織必須在某個時間點完成。在.Net中,這意味着可以剛好在編譯完成后進行編織(編譯時編織),或者可以在代碼執行期間的某個時間點進行編織(運行時編織)。
下面先來看看最簡單的運行時編織。
運行時編織
運行時編織,即編織發生在程序開始運行之后。在其他代碼應用切面代碼的同時,才會去實例化一個切面。這就是為什么Castle DynamicProxy測試友好的原因,沒到運行時,什么都不會發生。
運行時編織工作的方式類似於上面的裝飾者/代理模式,但是不需要手動創建裝飾類,運行時編織器會在運行時創建這些類。如上圖所示,我們仍然只需要創建分離的BusinessModule
和 LogAspect
,但是在運行時,其他類BusinessModuleProxy
(姑且稱之為)會被創建,用於裝飾BusinessModule
。
如果之前使用過代理模式或者裝飾者模式,那么上面的圖你會很熟悉。關鍵區別在於你不需要手動創建代理類BusinessModuleProxy
。如果不熟悉也沒關系,下一小節會針對這個有用的軟件設計模式進行一個新手的講解。
復習代理模式
討論動態代理之前先來復習一下代理模式的運行原理。代理模式和裝飾者模式都是設計模式,只是有稍微不同的目的和實現,但從AOP的角度看,實際上是一樣的。它們都運行你將功能添加到某個類而不需要改變類本身的代碼。一般情況下,代理用於另一個對象的替身,通常對實例化真正的對象負責,它和真實的對象有相同的接口。它可以控制訪問或提供附加的功能,以及對真實的對象進行控制。
對外來說,所有的程序只知道它正在使用一個具體的接口調用一個對象上的Method1
方法。這個對象就是一個代理,在調用真正的方法 Method1
之前,它有機會運行自己的代碼。一旦方法Method1
執行完成,它又有機會運行自己的代碼,最后才會返回原始程序的執行結果。
代理模式通常用於給程序表示外部的對象或服務(比如,web service)。在某些程序中,可能會給你一個生成的WCF代理類,它表示某個對象,你可以想象它就在你的程序中運行那樣操作它,但在該接口的背后,該代理會發送HTTP調用來完成你的指令。
裝飾者模式,在和真實的對象都有相同的接口方面,和代理模式是類似的。但是通常它不對實例化對象負責,因此多個裝飾器可以分層在真實對象的頂部。除了LogAspect
,還可以有 CacheAspect
。它們都有和 BusinessModule
相同的接口,以及自己的 BeginMethod
和 EndMethod
代碼。
從類似AOP功能的角度講,代理模式和裝飾器模式幾乎是一樣的模式。下面通過一個控制台程序演示一下代理模式:
using static System.Console;
namespace ProxyPatternReview
{
public interface IBusinessModule
{
void Method1();
}
public class BusinessModule : IBusinessModule
{
public void Method1()
{
WriteLine(nameof(Method1));//輸出方法名稱
}
}
}
using static System.Console;
namespace ProxyPatternReview
{
class Program
{
static void Main(string[] args)
{
IBusinessModule bm = new BusinessModule();
bm.Method1();
ReadKey();
}
}
}
上面代碼很簡單,不多說。現在創建一個扮演BusinessModule
代理的類 BusinessModuleProxy
,它實現了相同的接口 IBusinessModule
,這意味着我們只需要修改上面的 new
語句代碼即可(現實中要修改IoC配置)。
IBusinessModule bmProxy = new BusinessModuleProxy();
bmProxy.Method1();
就Main
方法而言,它不關心會獲得該模塊的任何對象,只要該對象的類實現了 IBusinessModule
接口就行。下面是 BusinessModuleProxy
的定義,記住它的工作是 BusinessModule
的替身,因此它要實例化 BusinessModule
,然后繼續執行 BusinessModule
的方法。
public class BusinessModuleProxy : IBusinessModule
{
BusinessModule _bm;
public BusinessModuleProxy()
{
_bm = new BusinessModule();
}
public void Method1()
{
_bm.Method1();
}
}
這個類幾乎是無用的,除了是Main和BusinessModule
的中間人之外,沒有其他目的。但是你可以在調用真實的方法Method1
之前和之后放任何你想執行的代碼,如下所示:
public void Method1()
{
WriteLine($"{nameof(Method1)} begin!");
_bm.Method1();
WriteLine($"{nameof(Method1)} end!");
}
看着很熟悉吧?這個代理對象正在扮演攔截切面的角色。我們可以將它用於緩存、日志、線程以及其他任何攔截切面可以實現的東西。只要Main方法獲得了一個IBusiness對象(很可能通過IoC容器),無論是否使用了代理類對象,它都會工作。而且,無需改變BusinessModule的任何代碼就可以將橫切關注點加入真實的BusinessModule。
但等一下,既然代理類能做AOP工具的事情,那么要AOP干什么?在一個有限的環境中,單獨地使用代理模式是有效的。但是如果要寫一個用於具有不同接口的多個類,那就需要為每個接口都要寫代理類了,是不是浪費生命?
如果你只有很少數量的類並且每個類有很少數量的方法,那么使用代理類沒多大問題。對於像日志和緩存這樣的橫切關注點,編寫大量的相似的功能性代理類會變得很重復。比如,為兩個具有兩個方法的接口編寫代理類不困難,但想一下,如果有12個接口呢,每個接口又有12個方法,那么就要編寫接近144個一樣的代理類方法了。
也想想,在一種不確定數量的類需要橫切關注點時,比如日志項目可能本身會復用到多個解決方案。通過使用動態代理,就不需要自己手動寫所有的這些代理了,只需要讓動態代理生成器幫助你工作即可。
動態代理
雖然代理模式不依賴第三方工具就可以實現關注點分離,但是在某些時候需要確定下代理模式本身會變得太重復和模板化。如果你發現自己經常在寫一些幾乎一樣的代理類,只是名字和接口稍微不同而已,那么是時候讓工具來為你完成這個工作了。Castle DynamicProxy (以及其他的使用了運行時編織的AOP工具)會通過Reflection, 特別是 Reflection.Emit來生成這些類。不用再在一個代碼文件中定義類了,代理生成器會使用Reflection.Emit API來創建類的。
來看一個類似於之前的代理模式的場景,以發微博為例。定義一個簡單的接口ISinaService
,它有一個發送微博的方法,然后創建該接口的實現類 MySinaService
,為了演示需要,只將發送內容輸出到控制台:
using static System.Console;
namespace DynamicProxyPractice
{
public interface ISinaService
{
void SendMsg(string msg);
}
public class MySinaService:ISinaService
{
public void SendMsg(string msg)
{
WriteLine($"[{msg}] has been sent!");
}
}
}
要使用代理模式或裝飾者模式,需要創建一個實現了該接口的類,暫且稱之為MySinaServiceProxy
,它要對創建真實的對象負責,並且可以實現自己的任何的代碼,下面只會在運行真實的對象方法之前和之后輸出相應信息,但在真實程序中,你可以實現日志,緩存等等功能:
public class MySinaServiceProxy : ISinaService
{
private MySinaService _service;
public MySinaServiceProxy()
{
_service = new MySinaService();
}
public void SendMsg(string msg)
{
WriteLine("Before");
_service.SendMsg(msg);
WriteLine("After");
}
}
問題來了,如果這個服務類有十幾個方法,那么意味着我們這個代理類也要有十幾個方法。或者,當前這個代理類只適合發微博,那么發微信呢,其他社交媒體呢?所以,每次真實的服務對象要添加或修改方法,你的代理類也必須做相應修改。相反,我們可以在運行時生成那些類。
個人代理生成器
這一小節我們會使用最原始的方法Reflection.Emit生成代理類。它不是動態的,因為它只給
MySinaService
生成了代理,做個形象的比喻,如果DynamicProxy是趟快車,那我們這個工具只是個石頭輪子(還比不上木頭輪子)。
這個例子不是打算教大家從頭開始寫自己的代理生成器,而是讓大家明白像Castle DynamicProxy這樣高級的工具是如何運作的。
因為Reflection.Emit會生成MySinaServiceProxy
,所以不需要在源碼中編寫了。相反,下面創建了一個返回類型為 MySinaServiceProxy
的方法,通過 Activator.CreateInstance
和該這個返回類型我們可以創建一個新實例。下面就在Mian方法中完成這個代理生成器:
static void Main(string[] args)
{
//生成一個動態代理類型並返回
var type = CreateDynamicProxyType();
//使用Activator和上面的動態代理類型實例化它的一個對象
var dynamicProxy = Activator.CreateInstance(type,new object[] { new MySinaService()}) as ISinaService;
//調用真實對象的方法
dynamicProxy.SendMsg("test msg");
ReadLine();
}
private static Type CreateDynamicProxyType()
{
//所有的Reflection.Emit方法都在這里
}
在運行時構建新類型和運行時構建新類型是相似的:
- 創建一個程序集;
- 在程序集中創建一個模塊;
- 使用Reflection.Emit API,創建一個AssemblyName,然后用它在當前域中定義一個AssemblyBuilder,然后使用該AssemblyBuilder創建一個ModuleBuilder。如下所示:
private static Type CreateDynamicProxyType()
{
//所有的Reflection.Emit方法都在這里
//1 定義AssemblyName
var assemblyName = new AssemblyName("MyProxies");
//2 DefineDynamicAssembly為你指定的程序集返回一個AssemblyBuilder
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
//3 使用AssemblyBuilder創建ModuleBuilder
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MyProxies");
}
模塊名稱和程序集名稱可以不一樣,這里只是為了簡單。
程序集和模塊
- 程序集是編譯后的代碼類庫,包括exe和dll文件。
- 程序集包含了一些元數據,並且可能包含一個或多個模塊,但在實踐中很少包含多個模塊。
- 模塊包含類。
- 類包含成員(方法或字段)。
一旦有了ModuleBuilder,就可以用它來構建一個代理類。要構建一個類型Type,需要一個類型名稱,特性(public和class),基類(所有類都有基類,即使是object)以及該類型實現的任何接口(這里我們要實現ISinaService)。明白了這些,然后使用ModuleBuilder的DefineType方法,它會返回一個TypeBuilder對象,如下代碼:
private static Type CreateDynamicProxyType()
{
//... 省略上面的代碼
TypeBuilder typeBuilder = moduleBuilder.DefineType(
"MySinaServiceProxy",//要創建的類型的名稱
TypeAttributes.Public|TypeAttributes.Class,//類型的特性
typeof(object),//基類
new[] {typeof(ISinaService)}//實現的接口
);
}
現在定義了一個類,但它是空的,我們還需要定義字段,構造函數和方法。先從字段開始,這個字段是用來存儲真實對象的,以便在代理想調用它時使用。要創建字段,需要字段名稱,類型(MySinaService)和特性(這里private)。將這些信息在TypeBuilder的DefineField方法中進行設置,就會返回一個FieldBuilder對象,如下:
FieldBuilder fieldBuilder = typeBuilder.DefineField(
"_realObject",
typeof(MySinaService),
FieldAttributes.Private
);
此時,這個方法會生成相應的下面的C#代碼,只是這種方式更冗長,因為我們做了和編譯器通常會為我們做的相似的工作。
public class MySinaServiceProxy : ISinaService
{
private MySinaService _realObject;
}
下一步,需要構建構造函數,它有一個形參,構造函數體會把形參賦值給字段。我們可以再使用TypeBuilder定義構造函數。要定義它,需要特性(只能是public),調用約定(實例構造函數還是靜態構造函數)以及每個形參是參數類型(這里只有一個類型為SinaService的參數)。然后使用DefineConstructor方法來定義構造函數,一旦定義了構造函數,我們需要一種方法將代碼放入構造函數中,這里使用GetILGenerator
獲得構造函數的 ILGenerator
對象:
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.HasThis,
new[] {typeof(MySinaService)}
);
ILGenerator ilgenerator = constructorBuilder.GetILGenerator();
在構造函數中,我們只需要一個語句來將參數分配給該字段(如果你計數return的話,就是兩個語句,return在C#中是隱含的)。 在調用DefineConstructor時,我們創建了一個指定類型數組的參數,但請注意參數沒有名稱。 就. NET而言,這只有參數argument 1。(為什么是參數1而不是參數0?因為參數0是this - -當前的實例)。
要將代碼放在構造函數中,我們需要使用constuctorBuilder來發出公共中間語言(CIL)操作碼。 可能你認為到這里的一切都很復雜,其實這里真的很難。 沒有多少人是精通Reflection.Emit的專家,但是因為這是一個簡單的操作,我還是能夠正確分配OpCodes的。 它包含三個部分:參數0(this),參數1(參數的輸入值)和將被賦值的字段。 它們被發送到計算堆棧,所以排序可能看起來很別扭。
//將this加載到計算棧
ilgenerator.Emit(OpCodes.Ldarg_0);
//將構造函數的形參加載到棧
ilgenerator.Emit(OpCodes.Ldarg_1);
//將計算結果保存到字段
ilgenerator.Emit(OpCodes.Stfld, fieldBuilder);
//從構造函數返回
ilgenerator.Emit(OpCodes.Ret);
現在我們已經生成了一個有名稱,有一個命名的私有字段和在構造函數中設置私有字段的類型。 為確保此類型實現ISinaService接口,我們需要定義一個名為SendMsg的void方法,它有一個字符串參數,如下列表所示。 使用TypeBuilder的這個信息以及DefineMethod和DefineMethodOverride,我們還需要另一個ILGenerator將代碼發送到此方法的方法體中。
MethodBuilder methodBuilder = typeBuilder.DefineMethod(
"SendMsg",//方法名稱
MethodAttributes.Public | MethodAttributes.Virtual,//方法修飾符
typeof(void),//無返回值
new[] { typeof(string) }//有個字符串參數
);
//指定要構建的方法實現了ISinaService接口的SendMsg方法
typeBuilder.DefineMethodOverride(
methodBuilder,
typeof(ISinaService).GetMethod("SendMsg")
);
//獲取一個ILGenerator將代碼添加到SendMsg方法
ILGenerator sendMsgIlGenerator = methodBuilder.GetILGenerator();
現在我們有一個SendMsg方法,我們需要填寫代碼。 在MySinaServiceProxy中,SendMsg方法將“Before”輸出到Console,然后調用真實的SendMsg方法,隨后將“After”寫入控制台。 我們需要通過發射OpCodes來處理所有這些事情,如該列表所示。
//加載字符串變量到計算棧
sendMsgIlGenerator.Emit(OpCodes.Ldstr, "Before");
//調用Console類的靜態WriteLine方法
sendMsgIlGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
//將參數argument0(this)加載到棧
sendMsgIlGenerator.Emit(OpCodes.Ldarg_0);
//將字段_realObject加載到棧
sendMsgIlGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
//加載SendMsg的參數到棧
sendMsgIlGenerator.Emit(OpCodes.Ldarg_1);
//調用字段上的SendMsg方法
sendMsgIlGenerator.Emit(OpCodes.Call, fieldBuilder.FieldType.GetMethod("SendMsg"));
//加載字符串After到棧
sendMsgIlGenerator.Emit(OpCodes.Ldstr, "After");
//調用Console類的靜態WriteLine方法
sendMsgIlGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
//返回
sendMsgIlGenerator.Emit(OpCodes.Ret);
就這樣 ,TypeBuilder對象具有構建我們想要的代理所需的所有信息。最后一步是使用構建器創建類型(並返回它):
return typeBuilder.CreateType();
Opcodes MSDN 文檔
點擊查看
運行結果見下圖,是不是有種既滿足(按照預期)又失望(太費力了)的感覺?
正如我之前所說,我們本章遠遠還不能構建一個完整的動態代理生成器。 要做這個小演示成為有點像樣的動態代理生成器將需要大量工作,包括(但不限於):
- 使其能夠代理任何類型,而不是只有MySinaService對象。
- 使其能夠處理這些對象中的任何方法,而不僅僅是SendMsg方法。
- 使其能夠執行任意的切面代碼,而不是僅僅在控制台輸出點東西。
- 將其全部包裝在一個漂亮的,封裝的,易於使用的API中。
幸運的是,諸如DynamicProxy這樣的工具為我們打開了這條路,所以我們沒有必要做所有這些繁瑣的管道。 通過向你展示這個過於簡單的動態代理版本,我希望能夠完成兩件事情:可以看到專門知識和復雜的工作,已經進入這些工具的制作之中,並給予你機會看看動態代理生成的底層原理。 當實現一個IInterceptor並將其提供給DynamicProxy ProxyGenerator時,其實你正在使用Reflection.Emit在運行時開始一系列復雜的程序集,模塊,類型,領域和方法構建來創建一個新的類型,但這些不存在於源代碼中。
編譯時編織工具的工作原理類似於運行時編織,除了它不會在運行時創建一個新的類型,我們在本章中一直在討論。它會在執行代碼之前修改由正常的.NET編譯器創建的程序集中的類型。
編譯時編織
當你在C#中創建一個.NET項目時,它被編譯成CIL(也稱為MSIL,IL和字節碼)然后成為程序集(DLL或EXE文件)。 下圖說明了流程的這個過程。 公共語言運行時(CLR)然后將CIL轉換成實際的機器指令(通過稱為即時編譯的過程,或JIT)。 作為.NET開發人員,這個過程應該是你熟悉的。
使用編譯時編織的AOP工具為此過程提供了另一個步驟稱為后期編譯(因此名稱PostSharp)。 完成編譯后,PostSharp(或其他編譯時的AOP工具)然后為已經創建的切面以及你已經指出切面用在了什么地方去檢查程序集。 然后它直接修改程序集中的CIL來執行編織,如圖所示。
這種方法的一個很好的副作用是PostSharp可以檢測到的任何錯誤也可以在Visual Studio中顯示,就好像它們是來自編譯器的錯誤(關於更多詳細見下一章)
在編譯器完成創建CIL代碼之后,后期編譯器進程將立即根據你編寫的切面以及在哪里應用了那些切面運行和修改CIL代碼。 修改CIL的這個過程是任何編譯時AOP工具通用基礎,但在本節的其余部分,你將看到一些PostSharp具體運行的細節,以及最終修改后的CIL是什么樣子。
后期編譯(PostCompiling)
為了幫助你理解PostSharp,我們先來一步一步看看PostSharp是如何工作的。
第一步是在編譯之前,當然你會使用PostSharp.dll庫編寫切面,並指出那些方面應該用在什么地方(例如,指定具有特性的切入點)。所有PostSharp切面都是特性,通常不會自己執行(它們只是元數據)。下一步是在編譯項目后立即進行。
編譯器會查看你的源代碼並將其轉換成一個包含CIL的程序集。 之后,PostSharp后期編譯程序接管。 它會檢查你編寫的切面,指定的切面用在了什么地方,以及程序集的CIL。 然后PostSharp會做幾件事情:實例化切面,序列化切面,並修改CIL,以適當調用該切面。
當PostSharp完成工作后,序列化切面將被存儲為匯編中的二進制流(作為資源)。 此流將在運行時加載用於執行(並且還將執行其RuntimeInitialize方法)。為了幫助可視化此過程,下是使用偽代碼來表示項目的三個主要狀態的圖:你編寫的源代碼,由編譯器與PostSharp合作創建的程序集,以及由CLR執行的執行程序。
所有這些都可能聽起來有點復雜,所以為了進一步演示,讓我們回顧一下PostSharp來龍去脈的工作原理。 我們將通過使用反編譯器比較寫入編譯后的程序集中的源代碼。
來龍去脈
反編譯器是一個可以分析.NET程序集(如DLL或EXE文件)的工具,並將其從CIL轉換回C#代碼。 它反過來編譯(CIL到C#而不是C#到CIL)。 你可以使用各種反編譯工具來實現反編譯,而且它們都傾向於有一組共同的功能,但我在這里使用的工具叫做ILSpy。官網http://ilspy.net 。
為了演示,我要編寫一個簡單的程序,編譯它,並使用ILSpy反編譯。 起初,我不會使用任何AOP,這意味着我希望看到我的C#和我的反編譯的C#是相同的。 這是一個只有一個方法簡單的類,在Visual Studio中:
namespace BeforeAndAfter
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
然后我將其編譯(在我的項目的bin文件夾中的DLL或EXE文件中)。 如果我使用ILSpy打開它與導航到Program,然后ILSpy將顯示我下圖:
如果你使用其他工具,你可能看不到完全相同的東西。 可能會出現一個默認的無參數構造函數。 每個類都需要一個構造函數,並且由於我沒有明確定義一個構造函數,所以編譯器假定一個public,空方法體,無參數的構造函數。 反編譯時,ILSpy也會做出相同的假設。 除此之外,反編譯的C#應該看起來和原來的C#相似,不管是你使用哪一個工具。
現在讓我們使用PostSharp在這個項目的代碼中添加一個切面。 PostSharp會修改CIL,這意味着我不會指望反編譯的C#與Visual Studio中看到的C#看起來相同。 以下列表再次顯示Program,這次用一個簡單的PostSharp方法應用到Main方法:
class Program
{
[MyAspect]
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
using PostSharp.Aspects;
namespace BeforeAndAfter
{
[Serializable]
public class MyAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Console.WriteLine("Before");
}
public override void OnExit(MethodExecutionArgs args)
{
Console.WriteLine("After");
}
}
}
編譯完后,我再次使用ILSpy打開程序集,看看反編譯Main代碼 如果你使用免費的PostSharp Express,你會看到代碼很長的方法(與完整商業版相比)。
PostSharp 功能:Aspect切面優化器
PostSharp Express不包括Aspect優化器。 Aspect優化器會檢查你編寫的切面,並修改IL只能完成任務你想做的事情。 例如,如果你根本不使用args對象,那么這個切面優化器將會發現這一點,當OnEntry和OnExit被調用時,一個空值將是傳遞給args參數。 另一個例子:因為我們沒有重寫OnSuccess或OnException,所以aspect優化器會看到這一點,並且在編織創建代碼時不會調用那些空的基本方法。
PostSharp Express沒有這個優化器 - 它假設你需要所有的東西,這就是為什么反編譯版本的Main方法太長了。
從上圖可以看到,編譯后的程序集包含兩個命名空間,一個是我們定義的命名空間BeforeAndAfter,另一個是PostSharp生成的隨機命名空間PostSharp.ImplementationDetails_f1559a2f,里面包含了PostSharp的詳細實現代碼,大概過程就是將元數據通過二進制序列化器反序列化為對應的我們定義的切面對象,然后再在Program程序中引入 PostSharp.ImplementationDetails_f1559a2f
命名空間,調用我們的切面的方法。Program類編織后的代碼和預想的差不多,但是PostSharp隨機生成的命名空間的代碼是AOP實現的關鍵。
命名可能看起來很奇怪。 這些都是怪異的名字,但它們只是名稱,像任何其他類或方法名稱一樣。 OnEntry是一個名為a0對象的方法,它是MyAspect類型。 對象a0是一個內部隱藏類<>z__a_1的內部靜態只讀成員。
PostSharp在編譯時通過添加和操作CIL,創建了這段代碼中的幾乎所有內容,這些都是根據你寫的切面而生成的。 一些生成的CIL不直接對應C#。 名稱<>z__a_1在C#中無效。 這些都是ILSpy盡力解讀的表現。
這部分和前一段可能似乎是深入.NET的底層,但現實中我們很少接觸到Reflection.Emit和CIL的操縱。 幸運的是,我們作為AOP工具的用戶 - 大多數時候不需要關心這樣的復雜性。 但重要的是要有一些對這些AOP實現的內部工作的理解,因為我們要對下決定使用哪種類型的AOP負責。 我們應該使用運行時編織,還是應該使用編譯時編織?
運行時編織 VS. 編譯時編織
開發人員似乎擔心的一個因素是性能,所以讓我們從通過比較兩種方法的性能入手。 根據我的經驗,現實是,程序中的性能瓶頸很少由使用AOP工具引起,而與在開發人員的生產力和可維護的代碼受益方面相比,AOP造成的任何性能問題都不重要。
如你所見,運行時AOP工具如DynamicProxy使用Reflection.Emit,這可能是用戶注意到的慢操作,但一旦類型創建,它不需要再次創建,所以這個性能點相對可以忽略不計。 編譯時工具不會使用緩慢的Reflection.Emit操作,因為它在編譯時執行其工作。 可以看得到,開發人員在解決方案中擁有大量使用了PostSharp項目時,這會增加構建時間。這是最常見的關於后期編譯工具的抱怨。 但隨着PostSharp新版本的性能不斷提高,你可以配置大型多項目解決方案,以使PostSharp不執行那些不使用切面的項目。 如果性能是你的主要關注點,這兩種類型的工具都會以某種方式降低性能,盡管可能在實踐中注意到不是足夠慢。
因此,你如何決定哪個AOP實現更好:運行時編織或編譯時編織,只基於性能考慮? 你應該使用哪一個? 雖然很討厭這種回答,但它是真實的:這視情況而定。
如果你沒有使用很多切面,或者你沒有在許多class上使用它們,就可以用寫代理或裝飾器類,根本不用任何第三方的AOP工具。
但是,如果你的項目使用了很多橫切關注點,AOP肯定會對你有好處。 也許在運行時動態生成的類型,也許在編譯時修改CIL。 也許兩者都行。 讓我們看下每種方法的好處。
運行時編織優點
使用你已經看過的DynamicProxy等工具的主要優點之一是它很容易測試(參見單元測試章節)。 一個DynamicProxy攔截器可以在運行時輕松注入依賴關系,方便編寫切面獨立的測試。
第二,與PostSharp這樣的工具相比,像DynamicProxy這樣的運行時工具不需要后編譯過程。 你不需要單獨的EXE,使其在每個團隊成員的計算機和構建服務器上正確編譯。 因此可能更容易將AOP引入項目團隊和/或項目的構建服務器。
第三,因為方面在運行時才被實例化,你也可以保留在構建完成后配置切面的能力。 運行時你擁有一定的靈活性 - 比如,可以使用XML文件更改切面配置。
最后,雖然許可和成本是復雜的問題,但是DynamicProxy是一個世界一流的AOP框架,是一個免費的開源工具,所以我一定會將它作為運行時編織陣營的頭牌。 這些是運行時優於編譯時編織的關鍵領域。
編譯時編織優點
編譯時編織有一些不同的好處。 由於PostSharp這樣的工具運行的本質(通過在程序集文件中直接操作CIL),它們可以更強大。
首先,通過運行時編織,攔截器通常被應用於類的每個方法,即使你只對一個類感興趣。 使用PostSharp等工具可以使用更細粒度的控制來應用切面。
其次,使用運行時編織,你通常需要使用IoC容器來使用攔截方面。 但是,程序中的每個對象並不總是這樣通過IoC工具實例化。 例如,UI對象和域對象可能不適合或不能用容器實例化。 因此,PostSharp等工具具有運行時AOP工具不具備的附加功能。
如果你正在開發的項目沒有使用IoC工具,那么為了使用運行時AOP,你需要重新構建代碼才能使用IoC工具,然后才能開始使用AOP。 通過編譯時AOP工具,你可以立即開始獲得AOP的優勢。 我不是說你不應該使用IoC或其他依賴注入工具。無論你是否使用AOP, 依賴注入是一個非常有用的工具,可以讓你創建松散耦合,易於測試的代碼。 但不是你從事的每個代碼庫都有使用DI來構建,而且重構過程可能會慢而昂貴。
編譯時工具更強大的最后一點是,它允許任何代碼使用AOP:包括靜態方法,私有方法和字段(在第5章中見位置攔截)。
小結
最終,我不能單方面決定使用一種方法:只有你可以做出這個決定。 除了我提到的技術利弊之外,還要考慮整個非技術因素,包括許可,價格和支持。 我用了很多篇幅描述兩種主要的方法:運行時編織和編譯時編織。 在實踐中,你評估的各個工具可能有所不同。 這個工具有多成熟? 它的API可能會改變嗎? 它的API是否有意義? 你的團隊其余成員最適合什么? 你是從舊版代碼庫開始還是從頭開始創建一個新項目?
這些都是在你下決定時必須考慮的關鍵屬性。 但是,這些都在技術之外,因為每個團隊,每個公司,每一個AOP工具,每個項目都不同。 現在你熟悉使用AOP和AOP工具如何工作,你對於項目或代碼庫的架構就會有整體的決定。
除了我一直在描述的常見橫切關注點的切面,AOP具有架構師感興趣的功能。由於PostSharp在編譯之后會對代碼進行檢查,因此它可以提供許多其他AOP工具無法提供的額外功能。 在下一章,我們來看看如何將PostSharp引入到你的架構。