說說emit(上)基本操作
文/玄魂
最近收到《.NET 安全揭秘》的讀者的郵件,提到了書中很多大家想看到的內容卻被弱化了,我本想回復很多內容因為書的主旨或者章節規划的原因只是概說性的,但是轉念一想,讀者需要的,不正是作者該寫的嗎?因此我准備把郵件中的問題一一搬到博客中,以博文的形式分享給大家。
今天要談論的主題是Emit,反射的孿生兄弟。想要通過幾篇博客詳盡的講解Emit也是很困難的事情,本系列計划通過完成一個簡單的Mock接口的功能來講解,計划寫三篇博客:
1) 說說Emit(上)基本操作;
2) 說說Emit (中)ILGenerator;
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。 |
|
在給定類型名稱、類型特性和已定義類型擴展的類型的情況下,構造 TypeBuilder。 |
|
在給定類型名稱、特性、已定義類型擴展的類型和類型的總大小的情況下,構造 TypeBuilder。 |
|
在給定類型名稱、特性、已定義類型擴展的類型和類型的封裝大小的情況下,構造 TypeBuilder。 |
|
在給定類型名稱、特性、已定義類型擴展的類型和已定義類型實現的接口的情況下,構造 TypeBuilder。 |
|
DefineType(String, TypeAttributes, Type, PackingSize, Int32) |
在給定類型名稱、特性、已定義類型擴展的類型,已定義類型的封裝大小和已定義類型的總大小的情況下,構造 TypeBuilder。 |
通過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):
名稱 |
說明 |
使用指定的名稱和方法特性向類型中添加新方法。 |
|
使用指定名稱、方法特性和調用約定向類型中添加新方法。 |
|
使用指定的名稱、方法特性和調用約定向類型中添加新方法。 |
|
DefineMethod(String, MethodAttributes, CallingConventions, Type, Type[]) |
使用指定的名稱、方法特性、調用約定和方法簽名向類型中添加新方法。 |
使用指定的名稱、方法特性、調用約定、方法簽名和自定義修飾符向類型中添加新方法。 |
如果需要定義構造函數,可以使用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類。