分类: 嵌入式
2012-06-23 12:36:58
文/玄魂
最近收到《.NET 安全揭秘》的读者的邮件,提到了书中很多大家想看到的内容却被弱化了,我本想回复很多内容因为书的主旨或者章节规划的原因只是概说性的,但是转念一想,读者需要的,不正是作者该写的吗?因此我准备把邮件中的问题一一搬到博客中,以博文的形式分享给大家。
今天要谈论的主题是Emit,反射的孪生兄弟。想要通过几篇博客详尽的讲解Emit也是很困难的事情,本系列计划通过完成一个简单的Mock接口的功能来讲解,计划写三篇博客:
1) 说说Emit(上)基本操作;
2) 说说Emit (中) ILGenerator;
3) 说说Emit (下) Emit在AOP和单元测试中的应用;
这几篇博客不可能涵盖Emit所有内容,只希望能让您知道Emit是什么,有哪些基本功能,如何去使用。
第一个需要动态实现接口的需求,是我在开发中遇到的,具体的业务场景会在《说说Emit (下) Emit在AOP和单元测试中的应用》中细说,先简要描述代码级别要实现的内容。首先我们有类似图1所示的以Before和After结尾的成对出现的方法若干。
图1 若干成对方法
我们根据一定的规则对上图所示的方法进行分类(分类的规则暂且不提),在实际调用过程中,不会直接调用上面的方法,而是调用一个名为IAssessmentAopAdviceProvider的接口的实例,该接口定义如下:
负责创建该接口的工厂类定义如下:
该工厂的职责是根据传入的参数,选择类似图1中的合适的成对方法动态创建一个IAssessmentAopAdviceProvider接口的实例,然后返回供调用方使用。当然如果不使用Emit也能实现这样的需求,这里我们只讨论使用Emit如何实现。
第一个需求简单介绍到这里,我们看第二个需求。现在我要在单元测试中测试某个依赖IAssessmentAopAdviceProvider的类,我们控制IAssessmentAopAdviceProvider的行为该怎么办呢?如果你做过单元测试,一定会想到Mock,我们可以使用Moq:
现在我也想实现这样的功能,该怎么做呢?您先不要惊讶,实现完整的Mock功能要实现一整套动态代理的框架,我还没这个雄心壮志,这里为了演示Emit,我以最简单的方式实现对IAssessmentAopAdviceProvider接口的Before方法的Mock,而且只针对某个特例,只保证这个特例能被调用即可。感兴趣的读者可以去读一读Moq的源码。
OK,技术需求到此结束,下面我们开始动手吧!
终于进入正题了,对于第一个需求,我们要做的工作描述起来很简单,创建一个类,实现IAssessmentAopAdviceProvider接口,期望结果如下:
public object After(object beforeResult, object value = null) { MvcAdviceReportProvider. DeleteUserResultAfter(beforeResult ,value); } }
上面代码中方法体内部的调用,工厂类会根据规则动态变更,这里我们先只考虑这个特例情况。
首先必要创建类AssessmentAopMvcAdviceProvider,想要创建类型,必要先有模块,想要有模块必须 先有程序集,所以我们要先创建程序集。
先看代码清单2-1。
代码清单2-1 创建程序集
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:程序集是持久化的,保存之前不可以执行代码。
创建了程序集之后,我们继续向程序集中添加模块。
我们使用如代码清单2-2的方式向程序集中添加模块。
代码清单 2-2
在代码清单2-2中,我们使用AssemblyBuilder.DefineDynamicModule 方法来创建模块,该方法共有三个重载,如下表所示:
名称 |
说明 |
|
定义指定名称的模块。 |
|
定义指定名称的模块,并指定是否发出符号信息。 |
|
定义持久模块。用给定名称定义将保存到指定文件路径的模块。 不发出符号信息。 |
|
定义持久模块,并指定模块名称、用于保存模块的文件名,同时指定是否使用默认符号编写器发出符号信息。 |
模块定义完成之后,到了略微关键的一步,定义类型。我们要定义的类型必须继承并实现IAssessmentAopAdviceProvider接口。实现代码如清单2-3。
代码清单2-3
AssemblyBuilder assemblyBuilder= AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider"); TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public, typeof(object), new Type[] { typeof(IAssessmentAopAdviceProvider) }); } } }
上述代码中mb.DefineType方法返回一个TypeBuilder实例,该方法有6个重载方法,这里采用的方法有四个参数,第一个参数是类型名称,第二个参数的TypeAttributes枚举是类型的访问级别和类型类别等其他信息,第三个参数是类型继承的基类,第四个参数是类型实现的接口。其他重载函数的说明如下(引自MSDN):
|
在此模块中用指定的名称为私有类型构造 TypeBuilder。 |
|
在给定类型名称和类型特性的情况下,构造 TypeBuilder。 |
|
在给定类型名称、类型特性和已定义类型扩展的类型的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型和类型的总大小的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型和类型的封装大小的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型和已定义类型实现的接口的情况下,构造 TypeBuilder。 |
|
在给定类型名称、特性、已定义类型扩展的类型,已定义类型的封装大小和已定义类型的总大小的情况下,构造 TypeBuilder。 |
通过TypeBuilder,可以使用TypeBuilder.DefineField来定义字段,使用TypeBuilder.DefineConstructor来定义构造函数,使用TypeBuilder.DefineMethod来定义方法,并使用TypeBuilder.DefineEvent来定义事件等,总之可以定义类型里的任何成员。这里我们只需要定义方法,如代码清单2-4所示。
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider"); TypeBuilder typeBuilder = moduleBuilder.DefineType("MvcAdviceProvider", TypeAttributes.Public, typeof(object), new Type[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), new Type[] { typeof(object)}); } } }
在上面的代码中,使用TypeBuilder.DefineMethod 方法来创建MethodBuilder对象。该方法有5个重载,如下表(引自MSDN):
名称 |
说明 |
|
使用指定的名称和方法特性向类型中添加新方法。 |
|
使用指定名称、方法特性和调用约定向类型中添加新方法。 |
|
使用指定的名称、方法特性和调用约定向类型中添加新方法。 |
|
使用指定的名称、方法特性、调用约定和方法签名向类型中添加新方法。 |
|
使用指定的名称、方法特性、调用约定、方法签名和自定义修饰符向类型中添加新方法。 |
如果需要定义构造函数,可以使用DefineConstructor和DefineDefaultConstructor方法。
在定义了方法之后,还可以使用MethodBuilder. SetSignature方法设置参数的数目和类型。MethodBuilder.SetParameters方法会重写TypeBuilder.DefineMethod 方法中设置的参数信息。当我们的方法接收泛型参数的时候,需要使用MethodBuilder.SetParameters方法来设定泛型参数。
定要了方法,还没有方法体,方法体需要使用ILGenerator类向其中注入il代码。ILGenerator的使用,我们单独放在下一篇博客中,Emit的方法调用的内容会放在第三篇博客中。
现在我们在Main方法中,输出我们刚才创建的程序集的信息,看看创建是否成功。
class Program {
static void Main(string[] args) { AssemblyName assemblyName = new AssemblyName("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), new Type[] { typeof(IAssessmentAopAdviceProvider) }); MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), new Type[] { typeof(object)}); MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), new Type[] { typeof(object), typeof(object) });
TestType(typeBuilder); }
private static void TestType(TypeBuilder typeBuilder) { Console.WriteLine(typeBuilder.Assembly.FullName); Console.WriteLine(typeBuilder.Module.Name); Console.WriteLine(typeBuilder.Namespace); Console.WriteLine(typeBuilder.Name); Console.Read(); } }
此时方法只有定义,还没有方法体,所以还不能创建类型的实例,显示结果如下:
还记上面提到的工厂类和要实现的目标代码吧,因为还没有描述业务场景,我们先不着急实现它的完整功能,现在不需要它接收任何参数,返回一个特定的IAssessmentAopAdviceProvider接口实例即可。雏形代码如下:
public static AdviceProviderFactory() { instanceDic = new Dictionary<string, IAssessmentAopAdviceProvider>(); assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); moduleBuilder = assemblyBuilder.DefineDynamicModule("MvcAdviceProvider");
}
internal static IAssessmentAopAdviceProvider GetProvider() { //创建接口的实例 return CreateInstance("MvcAdviceReportProvider"); }
private static IAssessmentAopAdviceProvider CreateInstance(string instanceName) { if (instanceDic.Keys.Contains(instanceName)) { return instanceDic[instanceName]; } else {
TypeBuilder typeBuilder = moduleBuilder.DefineType("EmitTest.MvcAdviceProvider", TypeAttributes.Public, typeof(object), new Type[] { typeof(IAssessmentAopAdviceProvider) });
MethodBuilder beforeMethodBuilder = typeBuilder.DefineMethod("Before", MethodAttributes.Public, typeof(object), new Type[] { typeof(object) }); MethodBuilder afterMethodBuilder = typeBuilder.DefineMethod("After", MethodAttributes.Public, typeof(object), new Type[] { typeof(object), typeof(object) }); //todo:注入iL代码, Type providerType = typeBuilder.CreateType(); IAssessmentAopAdviceProvider provider = Activator.CreateInstance(providerType) as IAssessmentAopAdviceProvider; instanceDic.Add(instanceName, provider); return provider;
} } }
这里只是做了一个简单的封装,没有做过多的其他内容,需要说明的是,通常我们会新建一个新的应用程序域来加载新建的程序集,然后通过透明代理来跨域访问。上面的代码仍然在当前上下文的应用程序域中创建程序集。
架子放在这,会在下一篇博客中,让它切实可用。
上面说到Mock类要实现的效果,我们也为它构建一个壳出来。代码如下:
private T ConfigObj(Mock
这是一个最简单的Mock,只能用来演示,甚至没任何实际应用价值。其中SetupContext对象用来记录执行Setup和Return扩展方法时的配置信息,定义如下:
此外定义了三个扩展方法,用来配置Mock行为,定义如下:
public static void Returns
public static MethodInfo ToMethodInfo(this LambdaExpression expression) { MemberExpression memberExpression = expression.Body as MemberExpression; if (memberExpression != null) { PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo; if (propertyInfo != null) { return propertyInfo.GetSetMethod(true); } } return null; } }
现在基本的壳已经有了,后续的实现也不会考虑的太复杂,只根据配置的方法名返回对应的返回值,不会考虑参数对结果的影响。这里把泛型类型约定为IAssessmentAopAdviceProvider,是为了演示方便,可以很方便的扩展为任意类型,不过实现起来也就复杂了。 Mock调用了AdviceProviderFactory来初始化对象的默认值,也就是说在默认情况下会走实际的代码逻辑。现在我们可以按如下方式使用这段代码了:
到目前为止,我们的准备工作已经完成了,仿佛正题还未开始,是不是太啰嗦了呢?下一篇博客,会专注于ILGenerator,并实现上面的工厂类和Mock类。