说说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