说说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 方法来创建模块,该方法共有三个重载,如下表所示:

名称

说明

DefineDynamicModule(String)

定义指定名称的模块。

DefineDynamicModule(String, Boolean)

定义指定名称的模块,并指定是否发出符号信息。

DefineDynamicModule(String, String)

定义持久模块。用给定名称定义将保存到指定文件路径的模块。不发出符号信息。

DefineDynamicModule(String, String, Boolean)

定义持久模块,并指定模块名称、用于保存模块的文件名,同时指定是否使用默认符号编写器发出符号信息。

模块定义完成之后,到了略微关键的一步,定义类型。我们要定义的类型必须继承并实现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):

DefineType(String)

在此模块中用指定的名称为私有类型构造 TypeBuilder

DefineType(String, TypeAttributes)

在给定类型名称和类型特性的情况下,构造 TypeBuilder

DefineType(String, TypeAttributes, Type)

在给定类型名称、类型特性和已定义类型扩展的类型的情况下,构造 TypeBuilder

DefineType(String, TypeAttributes, Type, Int32)

在给定类型名称、特性、已定义类型扩展的类型和类型的总大小的情况下,构造 TypeBuilder

DefineType(String, TypeAttributes, Type, PackingSize)

在给定类型名称、特性、已定义类型扩展的类型和类型的封装大小的情况下,构造 TypeBuilder

DefineType(String, TypeAttributes, Type, Type[])

在给定类型名称、特性、已定义类型扩展的类型和已定义类型实现的接口的情况下,构造 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)

使用指定的名称和方法特性向类型中添加新方法。

DefineMethod(String, MethodAttributes, CallingConventions)

使用指定名称、方法特性和调用约定向类型中添加新方法。

DefineMethod(String, MethodAttributes, Type, Type[])

使用指定的名称、方法特性和调用约定向类型中添加新方法。

DefineMethod(String, MethodAttributes, CallingConventions, Type, Type[])

使用指定的名称、方法特性、调用约定和方法签名向类型中添加新方法。

DefineMethod(String, MethodAttributes, CallingConventions, Type, Type[], Type[], Type[], 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类。

说说emit(上)基本操作相关推荐

  1. 线性表的链式表示——双链表

    定义 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱.所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点.一般我们都构造双向循 ...

  2. 线性表的链式表示——单链表

    单链表 定义 线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素.每个链表的结点,除存放元素自身的信息之外,还需要存放一个指向其后继结点的指针.即单链表的结构分为两部分, ...

  3. 进栈顺序为abcd则出栈顺序为_线性表之顺序表示

    线性表 1. 线性表的基础知识 1.1 线性表的定义 线性表是具有相同数据类型的n(n>0)个数据元素的有限序列. 若用L命名,表示:L=(a1,a2,a3,-,ai-1,ai,ai+1,-,a ...

  4. 如何直观的解释back propagation算法?

    作者:Evan Hoo 链接:http://www.zhihu.com/question/27239198/answer/89853077 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非 ...

  5. 算法导论第十二章:二叉查找树

    查找树是一种数据结构,它支持多种动态集合操作,包括search, minimum, maximum, predecessor, successor, insert以及delete.他既可以用作字典,也 ...

  6. Storm的acker确认机制

    Storm的acker消息确认机制... ack/fail消息确认机制(确保一个tuple被完全处理) 在spout中发射tuple的时候需要同时发送messageid,这样才相当于开启了消息确认机制 ...

  7. 厦门理工学院2019年数据结构与算法考研初试大纲

    厦门理工学院2019年专业学位硕士研究生复试考试专业课课程考试大纲 一.考试科目名称:数据结构与算法 二.招生系部和专业: 考试要求: 要求考生能比较全面的理解与掌握数据结构的基本概念.基本原理和基本 ...

  8. Python 调试方法

    FROM http://kamushin.github.io/debug/python.html 背景 这几天一直在查一个线上程序 hang 住的问题. 这个程序总是在运行50分钟后 hang 住, ...

  9. 深入理解 Node.js 中 EventEmitter源码分析(3.0.0版本)

    events模块对外提供了一个 EventEmitter 对象,即:events.EventEmitter. EventEmitter 是NodeJS的核心模块events中的类,用于对NodeJS中 ...

最新文章

  1. C中文件操作的文本模式和二进制模式,到底有啥区别?
  2. 【畅谈百度轻应用】云时代·轻应用·大舞台
  3. OpenWRT中运行脚本报错
  4. system verilog随机函数_systemverilog中的随机化激励.pdf
  5. SQL SERVER数据库多条件查询
  6. Find the safest road(HDU-1596)
  7. 怎么样做好手机网站的优化和推广呢?
  8. 云计算学习笔记002---云计算的理解及介绍,google云计算平台实现原理
  9. 可行后继路由,可行条件和报告距离
  10. IAR 软件激活步骤
  11. python 爬取 全网代理 IP 网站 + 破解端口加密混淆
  12. 用python在大麦网抢票_大麦网自动抢票工具
  13. QClub:Ruby网站架构案例分享──财帮子\u0026FreeWheel
  14. 跟键盘在一起时间比女朋友还长? 程序员如何选好一块专属键盘?
  15. redis—redis概述
  16. 前端生成二维码 微信小程序
  17. android 消息轮训,Android消息机制Handler,有必要再讲一次
  18. 计算机软件维护工程师工作总结,计算机维护工程师年终工作总结.docx
  19. 江苏大学张世兵计算机学院,高校辅导员法律意识存在的不足与提升路径.pdf
  20. 磁卡感应卡二合一读写器|写卡器Z100-RF

热门文章

  1. html 页面怎么自动定位到某个标签,JS如何实现在页面上快速定位(锚点跳转问题)...
  2. dplyr | 数据导入和预处理的常用函数
  3. 安装centos7步骤_Centos7下源码编译安装mysql5.7 详细步骤 小白也能安装
  4. golang 初始化并赋值_Golang 切片综合指南
  5. S - C语言实验——余弦
  6. python字典遍历 没有顺序_Python实现字典的遍历与排序功能示例
  7. return 0可以不加吗_拼多多开店可以不交保证金? 拼多多0元开店是真的吗?
  8. 动态加载html 添加样式表,使页面动态加载不同CSS样式表,从而实现不同风格模板的方法...
  9. 服务器传递文件丢包怎么回事,TCP传输协议中如何解决丢包问题
  10. python可视化的图表汉字显示成框框_Matplotlib图表上输出中文(汉字)、减号问题...