最近收到《.NET 安全揭秘》的讀者的郵件,提到了書中很多大家想看到的內 容卻被弱化了,我本想回復很多內容因為書的主旨或者章節規劃的原因只是概說 性的,但是轉念一想,讀者需要的,不正是作者該寫的嗎?因此我准備把郵件中 的問題一一搬到博客中,以博文的形式分享給大家。
今天要談論的主題是Emit,反射的孿生兄弟。想要通過幾篇博客詳盡的講解 Emit也是很困難的事情,本系列計劃通過完成一個簡單的Mock接口的功能來講解 ,計劃寫三篇博客:
2) 說說Emit (中)ILGenerator;
<p CxSpLast" style=";margin-left: 45pt">3) 說說Emit (下)Emit在AOP和單元測試中的應用;
這幾篇博客不可能涵蓋Emit所有內容,只希望能讓您知道Emit是什麼,有哪些 基本功能,如何去使用。
1.1 動態實現接口的技術需求
第一個需要動態實現接口的需求,是我在開發中遇到的,具體的業務場景會在 《說說Emit (下) Emit在AOP和單元測試中的應用》中細說,先簡要描述代碼級別 要實現的內容。首先我們有類似圖1所示的以Before和After結尾的成對出現的方 法若干。
圖1 若干成對方法
我們根據一定的規則對上圖所示的方法進行分類(分類的規則暫且不提),在 實際調用過程中,不會直接調用上面的方法,而是調用一個名為 IAssessmentAopAdviceProvider的接口的實例,該接口定義如下:
publicinterfaceIAssessmentAopAdviceProvider
{
object Before(object value);
object After(object beforeResult, object value);
}
負責創建該接口的工廠類定義如下:
staticclassAdviceProviderFactory
{
internalstaticIAssessmentAopAdviceProvider GetProvider(AdviceType adviceType, string instanceName,string funcName,MvcAdviceType mvcAdviceType)
{
//創建接口的實例
}
}
該工廠的職責是根據傳入的參數,選擇類似圖1中的合適的成對方法動態創建 一個IAssessmentAopAdviceProvider接口的實例,然後返回供調用方使用。當然 如果不使用Emit也能實現這樣的需求,這裡我們只討論使用Emit如何實現。
第一個需求簡單介紹到這裡,我們看第二個需求。現在我要在單元測試中測試 某個依賴IAssessmentAopAdviceProvider的類,我們控制 IAssessmentAopAdviceProvider的行為該怎麼辦呢?如果你做過單元測試,一定 會想到Mock,我們可以使用Moq:
Mock<IAssessmentAopAdviceProvider> assessmentAopAdviceProviderMocked = newMock<IAssessmentAopAdviceProvider>();
assessmentAopAdviceProviderMocked.Setup(t => t. Before (It.IsAny<object>())).Returns(expectObject);
現在我也想實現這樣的功能,該怎麼做呢?您先不要驚訝,實現完整的Mock功 能要實現一整套動態代理的框架,我還沒這個雄心壯志,這裡為了演示Emit,我 以最簡單的方式實現對IAssessmentAopAdviceProvider接口的Before方法的Mock ,而且只針對某個特例,只保證這個特例能被調用即可。感興趣的讀者可以去讀 一讀Moq的源碼。
OK,技術需求到此結束,下面我們開始動手吧!
1.2 動態創建完整的程序集
終於進入正題了,對於第一個需求,我們要做的工作描述起來很簡單,創建一 個類,實現IAssessmentAopAdviceProvider接口,期望結果如下:
publicclassAssessmentAopMvcAdviceProvider : IAssessmentAopAdviceProvider
{
publicobject Before(object value = null)
{
MvcAdviceReportProvider.DeleteUserResultBefore(value);
}
publicobject After(object beforeResult, object value = null)
{
MvcAdviceReportProvider.DeleteUserResultAfter (beforeResult ,value);
}
}
上面代碼中方法體內部的調用,工廠類會根據規則動態變更,這裡我們先只考 慮這個特例情況。
首先必要創建類AssessmentAopMvcAdviceProvider,想要創建類型,必要先有 模塊,想要有模塊必須先有程序集,所以我們要先創建程序集。
(注:下面的創建過程和說明改編自《.NET 安全揭秘》第二章)
先看代碼清單2-1。
代碼清單2-1 創建程序集
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
}
}
}
AppDomain.CurrentDomain.DefineDynamicAssembly方法返回一個 AssemblyBuilder實例。其中,第一個參數是AssemblyName實例,是程序集的唯一 標識;第二個參數AssemblyBuilderAccess.Run表明該程序集只能用來執行代碼, 不能被持久保存。AssemblyBuilderAccess還有如下選項:
q AssemblyBuilderAccess.ReflectionOnly:程序集只能在反射上下文 中執行。
q AssemblyBuilderAccess.RunAndCollect:程序集可以運行和垃圾回 收。
q AssemblyBuilderAccess.RunAndSave:程序集可以執行代碼而且被持 久保存。
q AssemblyBuilderAccess.Save:程序集是持久化的,保存之前不可以 執行代碼。
創建了程序集之後,我們繼續向程序集中添加模塊。
注:“程序集是.NET應用程序的基本單位,是CLR運行托管程序的最基本 單位。它通常的表現形式是PE文件,區分PE文件是不是程序集或者說模塊和程序 集的根本區別是程序集清單,一個PE文件如果包含了程序集清單那麼它就是程序 集。”----《.NET 安全揭秘》第二章
我們使用如代碼清單2-2的方式向程序集中添加模塊。
代碼清單 2-2
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
}
}
}
在代碼清單2-2中,我們使用AssemblyBuilder.DefineDynamicModule 方法來 創建模塊,該方法共有三個重載,如下表所示:
模塊定義完成之後,到了略微關鍵的一步,定義類型。我們要定義的類型必須 繼承並實現IAssessmentAopAdviceProvider接口。實現代碼如清單2-3。
代碼清單2-3
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
}
}
}
上述代碼中mb.DefineType方法返回一個TypeBuilder實例,該方法有6個重載 方法,這裡采用的方法有四個參數,第一個參數是類型名稱,第二個參數的 TypeAttributes枚舉是類型的訪問級別和類型類別等其他信息,第三個參數是類 型繼承的基類,第四個參數是類型實現的接口。其他重載函數的說明如下(引自 MSDN):
通過TypeBuilder,可以使用TypeBuilder.DefineField來定義字段,使用 TypeBuilder.DefineConstructor來定義構造函數,使用 TypeBuilder.DefineMethod來定義方法,並使用TypeBuilder.DefineEvent來定義 事件等,總之可以定義類型裡的任何成員。這裡我們只需要定義方法,如代碼清 單2-4所示。
namespace EmitTest
{
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object)});
}
}
}
在上面的代碼中,使用TypeBuilder.DefineMethod 方法來創建MethodBuilder 對象。該方法有5個重載,如下表(引自MSDN):
如果需要定義構造函數,可以使用DefineConstructor和 DefineDefaultConstructor方法。
在定義了方法之後,還可以使用MethodBuilder.SetSignature方法設置參數的 數目和類型。MethodBuilder.SetParameters方法會重寫 TypeBuilder.DefineMethod 方法中設置的參數信息。當我們的方法接收泛型參數 的時候,需要使用MethodBuilder.SetParameters方法來設定泛型參數。
定要了方法,還沒有方法體,方法體需要使用ILGenerator類向其中注入il代 碼。ILGenerator的使用,我們單獨放在下一篇博客中,Emit的方法調用的內容會 放在第三篇博客中。
現在我們在Main方法中,輸出我們剛才創建的程序集的信息,看看創建是否成 功。
classProgram
{
staticvoid Main(string[] args)
{
AssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
TypeBuilder typeBuilder = moduleBuilder.DefineType("EmitTest.MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object)});
MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), newType[] { typeof(object), typeof(object) });
TestType(typeBuilder);
}
privatestaticvoid TestType(TypeBuilder typeBuilder)
{
Console.WriteLine (typeBuilder.Assembly.FullName);
Console.WriteLine (typeBuilder.Module.Name);
Console.WriteLine (typeBuilder.Namespace);
Console.WriteLine (typeBuilder.Name);
Console.Read();
}
}
此時方法只有定義,還沒有方法體,所以還不能創建類型的實例,顯示結果如 下:
(這裡也留給大家一個小問題:為什麼上圖中輸出的模塊名稱是“在內 存模塊中”呢?)
1.3 構建工廠類雛形
還記上面提到的工廠類和要實現的目標代碼吧,因為還沒有描述業務場景,我 們先不著急實現它的完整功能,現在不需要它接收任何參數,返回一個特定的 IAssessmentAopAdviceProvider接口實例即可。雛形代碼如下:
publicstaticclassAdviceProviderFactory
{
staticDictionary<string, IAssessmentAopAdviceProvider> instanceDic;
staticreadonlyAssemblyName assemblyName = newAssemblyName("EmitTest.MvcAdviceProvider");
staticAssemblyBuilder assemblyBuilder;
staticModuleBuilder moduleBuilder;
publicstatic AdviceProviderFactory()
{
instanceDic = newDictionary<string, IAssessmentAopAdviceProvider>();
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
}
internalstaticIAssessmentAopAdviceProvider GetProvider()
{
//創建接口的實例
return CreateInstance ("MvcAdviceReportProvider");
}
privatestaticIAssessmentAopAdviceProvider CreateInstance(string instanceName)
{
if (instanceDic.Keys.Contains (instanceName))
{
return instanceDic [instanceName];
}
else
{
TypeBuilder typeBuilder = moduleBuilder.DefineType ("EmitTest.MvcAdviceProvider", TypeAttributes.Public,
typeof(object), newType[] { typeof (IAssessmentAopAdviceProvider) });
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), newType[] { typeof(object) });
MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), newType[] { typeof(object), typeof(object) });
//todo:注入iL代 碼,
Type providerType = typeBuilder.CreateType();
IAssessmentAopAdviceProvider provider = Activator.CreateInstance (providerType) asIAssessmentAopAdviceProvider;
instanceDic.Add (instanceName, provider);
return provider;
}
}
}
查看本欄目
1.4 構建Mock類雛形
上面說到Mock類要實現的效果,我們也為它構建一個殼出來。代碼如下:
publicclassMock<T> where T : IAssessmentAopAdviceProvider
{
public T Obj {
get { return ConfigObj(this); }
set; }
publicSetupContext Contex { get; set; }
public Mock()
{
Obj = (T) AdviceProviderFactory.GetProvider();
}
private T ConfigObj(Mock<T> mock)
{
returndefault(T);//這裡根據 SetupContext重新配置方法
}
}
這是一個最簡單的Mock,只能用來演示,甚至沒任何實際應用價值。其中 SetupContext對象用來記錄執行Setup和Return擴展方法時的配置信息,定義如下 :
publicclassSetupContext
{
publicstring MethodName { get; set; }
publicobject ReturnVlaue { get; set; }
}
此外定義了三個擴展方法,用來配置Mock行為,定義如下:
publicstaticclassMockExtention
{
publicstaticMock<T> Setup<T> (thisMock<T> mocker, Expression<Action<T>> expression)
{
mocker.Contex = newSetupContext ();
mocker.Contex.MethodName = expression.ToMethodInfo().Name;
return mocker;
}
publicstaticvoid Returns<T> (thisMock<T> mocker, object returnValue)
{
mocker.Contex.ReturnVlaue = returnValue;
}
publicstaticMethodInfo ToMethodInfo (thisLambdaExpression expression)
{
MemberExpression memberExpression = expression.Body asMemberExpression;
if (memberExpression != null)
{
PropertyInfo propertyInfo = memberExpression.Member asPropertyInfo;
if (propertyInfo != null)
{
return propertyInfo.GetSetMethod(true);
}
}
returnnull;
}
}
現在基本的殼已經有了,後續的實現也不會考慮的太復雜,只根據配置的方法 名返回對應的返回值,不會考慮參數對結果的影響。這裡把泛型類型約定為 IAssessmentAopAdviceProvider,是為了演示方便,可以很方便的擴展為任意類 型,不過實現起來也就復雜了。Mock調用了AdviceProviderFactory來初始化對象 的默認值,也就是說在默認情況下會走實際的代碼邏輯。現在我們可以按如下方 式使用這段代碼了:
Mock<IAssessmentAopAdviceProvider> mock = newMock<IAssessmentAopAdviceProvider>();
mock.Setup(t => t.Before(null)).Returns(new { a=""});
到目前為止,我們的准備工作已經完成了,仿佛正題還未開始,是不是太啰嗦 了呢?下一篇博客,會專注於ILGenerator,並實現上面的工廠類和Mock類。
者:玄魂
出處:http://www.cnblogs.com/xuanhun/