这篇文章会详细阐述如何使用 Emit、反射、表达式 为接口动态构建一个实现其的代理类,此代理类会在接口方法被调用时,将相关信息(方法、参数、接口类型)转至您事先构造好的 处理对象。
首先,我们先看一下类库的使用效果:
测试接口的定义:
public interface TestInterface
{
void VoidTest();
void VoidTestParameters(Int32 para1);
Object RetTest();
Object RetTestParameters(Int32 para1, Int32 para2);
}
另外是我们对接口方法调用的拦截处理:
/// <summary>
/// 代理类调用处理接口
/// </summary>
public interface IDynamicProxyHandler
{
/// <summary>
/// 处理对代理类方法的调用
/// </summary>
/// <param name="proxy">代理类的对象</param>
/// <param name="interfaceType">接口的类型</param>
/// <param name="interfaceInvokeMethod">调用的接口的方法</param>
/// <param name="parameters">方法参数列表</param>
/// <returns>方法的返回结果</returns>
Object InovkeHandle(Object proxy,
Type interfaceType,
MethodInfo interfaceInvokeMethod,
Object[] parameters);
}
public class DynamicProxyHandler : IDynamicProxyHandler
{
public Object InovkeHandle(Object proxy,
Type interfaceType,
MethodInfo interfaceInvokeMethod,
Object[] parameters)
{
Console.WriteLine("ProxyObjectType: " + proxy.GetType().ToString());
Console.WriteLine("InterfaceType: " + interfaceType.ToString());
Console.WriteLine("Method: " + interfaceInvokeMethod.ToString());
Console.Write("Parameters: ");
if (parameters != null)
{
foreach (var obj in parameters)
{
Console.Write(obj.ToString() + " ");
}
}
Console.WriteLine();
return null;
}
}
类库的调用过程:
public static void Main(params String[] args)
{
var interfaceObj = ClientProxyFactory.CreateProxy<TestInterface>(
new DynamicProxyHandler()
);
interfaceObj.VoidTest();
interfaceObj.VoidTestParameters(56);
interfaceObj.RetTest();
interfaceObj.RetTestParameters(12,63);
}
最后就是调用的效果了:
接下来我们详细说明,如何在运行时动态构建程序集、模块、类以及其类成员。
首先以一小段简易的 IL 代码开始我们的文章,Emit 技术中核心的一点便是在运行时使用 IL 构建需要的行为代码,正因为您是以 IL 的角度构建它们的,所以需要对其有所了解。(可以下载 Microsoft .NET IL汇编语言程序设计 以了解更多关于 IL 的信息。)
.assembly extern mscorlib{}
.assembly Example{}
.module Example.exe
.namespace Example
{
.class public auto ansi Program extends [mscorlib]System.Object
{
.method public static void Test() cil managed
{
.entrypoint
//Console.WriteLine("Hope Bridge!");
//Console.Read();
ldstr "Hope Bridge!"
call void [mscorlib]System.Console::WriteLine(string)
call int32 [mscorlib]System.Console::Read()
pop
ret
}
}
}
将代码保存至文本文件中,使用 ilasm 工具 ( 在安装 Visual Studio 开发工具后应该可以在 C:\Program Files (x86)\Microsoft SDKs\Windows 下找到不同版本的工具集的集合,例如我的 ilasm 的工具的就是在:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools 下,建议根据情况将其加入到环境变量以方便调用 ) 。
并键入指令 ilasm /exe yourcodefilename /output=yourouputfilename
例如:ilasm /exe example.il /output=Example.exe
*
编译这段代码,并得到可执行文件,执行其运行结果如下:
-
.assembly extern 定义了称为 AssemblyRef 的元数据项,此元数据项目记录了当前程序集对某个外部程序集的引用,可以将 .assembly extern mscorlib 这段代码的作用暂时等价理解为您在 C# 项目中使用工具手动为项目添加名为 mscorlib.dll (一般会被系统自动引用) 的程序集的引用。
-
.assembly Example{} 定义了称为配件的元数据项,您可以将其等价为在 C# 项目中设置程序集名称
-
.module Example.exe 一个程序集(配件)至少会有一个模块(当然配件可以由多个模块构成)您在这边文章里面只要记住我们需要声明它(详情可参见 《Microsoft .NET IL汇编语言程序设计》 第三部分-第五章 模块和配件)。
-
接下来的 .namespace 、.class、.method 、.entrypoint 分别等价为 C# 中的空间声明、类声明、方法声明、以及入口函数声明( 静态方法 Main,在IL中并没有限定入口函数的名称,这是 C# 的限定 )
-
IL 是一个严格的基于栈的语言,IL 指定不能直接寻址局部变量或者方法参数,只能从栈顶取走一些内容或者放入一些内容到栈顶。
所以我们在方法中使用到某些变量时,应该先使用指令将其先按顺序推送到栈顶( ldstr “Hope Bridge!” )。
然后下一个指令再从栈顶,按顺序取走自己所需的变量(call void [mscorlib]System.Console::WriteLine(string) ))。
最终,在函数结束时,应该保持栈为空( pop -因为 Console.Read() 函数在调用完成后会有一个 Int32 类型的变量的返回值,其会被推送到栈顶)。
并以 ret 指令表示函数结束。( 其他指令参见 .NET IL 指令集在Emit中的对应 )
好了,接下来我们根据以上示例使用 Emit 技术动态构建一个拥有相同的功能的程序集(不再指定入口点),并调用 Test 函数。
//建议您在阅读时,配合 MSDN 熟悉并具体每个方法
//这是必须的,我们的动态程序集需要明确的被加载到某个应用程序域中
AppDomain domain = AppDomain.CurrentDomain;
//此对象可以用来完整描述我们的动态程序集,这里只起名称就像 .assembly Example{} 一样,当然还有其他属性可以指定
AssemblyName name = new AssemblyName("Example");
//在我们的程序域中定义程序集,您完全可以使用其他的域,这么一来后续的通信也需要你处理
AssemblyBuilder assemblyBuilder = domain.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect);
//定义一个模块,我们说过一个程序集(配件) 至少需要一个模块
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("Example.exe");
//这里就开始构建我们的类型了
TypeBuilder typeBuilder = moduleBuilder.DefineType("Program");
//为类型定义一个方法
MethodBuilder methodBuilder = typeBuilder.DefineMethod("Test", MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard);
//最关键的来了,我们需要构建方法中的行为代码,使用 GetILGenerator 函数可以获取
ILGenerator iLGenerator = methodBuilder.GetILGenerator();
//推送一个字符串变量到栈顶 ldstr "Hope Bridge!"
iLGenerator.Emit(OpCodes.Ldstr, "Hope Bridge!");
//获取到我们想要调用的方法 Console.WriteLine(String)
MethodInfo writeLineMethod = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(String) });
//不用 OpCode 我们需要使用不同的 Emit 方法的重载,这里我们使用 ILGenerator.Emit(OpCode, MethodInfo) 这个重载
//从栈顶获取一个字符变量并执行方法 call void [mscorlib]System.Console::WriteLine(string)
iLGenerator.Emit(OpCodes.Call, writeLineMethod);
// 获取到我们想要调用的方法 Console.Read()
MethodInfo readMethod = typeof(Console).GetMethod("Read");
//call int32 [mscorlib]System.Console::Read()
iLGenerator.Emit(OpCodes.Call, readMethod);
//清空栈顶 pop
iLGenerator.Emit(OpCodes.Pop);
//结束方法 ret
iLGenerator.Emit(OpCodes.Ret);
//使用我们构建的类型信息创建类型对象
Type type = typeBuilder.CreateType();
//获取我们动态构建的类型的静态方法
MethodInfo method = type.GetMethod("Test");
//调用动态类型的方法:
method.Invoke(null, null);
然后我们执行这段C# 代码,得到与 IL 示例一样的结果:
使用 Emit 技术相比于单纯使用IL,您可以使用更高级的语言带来的各种便利,让两者结合使用,单不管哪种都需要对 IL 有一定了解,较为复杂的行为您可以事先写好同样行为的更高级语言(例如:C#、VB)代码,然后使用 ILDasm 工具反编译,之后参照反编译所得的 IL 代码,再次使用 Emit 构建。
这里只是让大家对 Emit 的简单使用、IL 有一个入门、了解,具体深入还需要靠大家自己研究。
接下着重于讲述拦截方法调用的思路以及对拦截后的调用处理。
大家都知道在 C# 中接口只是声明方法,而不定义具体的行为,我们对接口的使用往往是对实现其的类的对象的调用,使用接口很好的屏蔽了背后实现的差异,而接口的声明又描述了行为的规范,这让接口的调用者与实现者可以依赖其分工合作。
而这篇文章给出的拦截调用的方法,同样需要针对接口构建一个实现类,区别在于此处的构建工作在程序运行时,此项技术还以应用到对其他类型的对象的方法调用拦截上,您可以将散布在项目中诸多重复性代码(例如日志的记录的调用、异常统一捕获处理)使用此技术整理掉。针对于此项技术的应用还有很多,这里不再赘述。
开始正文(请您参照文章开篇的项目代码,以及文章中间给出的实现思路,阅读以下内容):
首先我们需要构建一个继承自接口的实现类型:
核心便是使用 ModuleBuilder.DefineType 的一个重载方法
TypeBuilder typeBuilder = moduleBuilder.DefineType(
typeName,
TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed,
objectType,
new Type[] { interfaceType });
构建好此类型之后,在为构建类型的方法中,我们需要将方法跳转到我们指定的代码中,此处抽象出一个接口:
/// <summary>
/// 代理类调用处理接口
/// </summary>
public interface IDynamicProxyHandler
{
/// <summary>
/// 处理对代理类方法的调用
/// </summary>
/// <param name="proxy">代理类的对象</param>
/// <param name="interfaceType">接口的类型</param>
/// <param name="invokeMethod">调用的接口的方法</param>
/// <param name="parameters">方法参数列表</param>
/// <returns>方法的返回结果</returns>
Object InovkeHandle(Object proxy, Type interfaceType, MethodInfo invokeMethod, Object[] parameters);
}
而正如文章开头所说,对接口的调用应该被转移到具体的实现类的对象上,但在使用 Emit 构建的方法中,我们应该如何使用处理接口的实现对象呢,结合常规的C#代码思路,我们可以为类型定义一个为 IDynamicProxyHandler 类型的字段,然后在方法中调用此字段的处理函数 InovkeHandle:
FieldBuilder handlerField = typeBuilder.DefineField(
field_handler_Name,
typeof(IDynamicProxyHandler),
FieldAttributes.Private);
而根据处理接口的声明我们需要传入接口的类型,为了方便,我们干脆再定义一个字段存储实现类的继承的接口的类型:
FieldBuilder interfaceTypeField = typeBuilder.DefineField(
field_interfaceType_Name,
interfaceType,
FieldAttributes.Private);
那么,为类型构建的这两个字段应该怎么赋值呢,相对来说最方便还是在构造函数中进行赋值操作:
//为类型构建构造函数信息
ConstructorInfo objectConstructorInfo = objectType.GetConstructor(new Type[] { });
//public DynamicProxy(IDynamicProxyHandler handler,Type interfaceType)
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
new Type[] { typeof(IDynamicProxyHandler), typeof(Type) });
ILGenerator iLGenerator = constructorBuilder.GetILGenerator();
//在构造函数中使用参数为私有字赋值
//this._handler=handler;
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Ldarg_1);
iLGenerator.Emit(OpCodes.Stfld, handlerField);
//this._interfaceType=interfaceType;
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Ldarg_2);
iLGenerator.Emit(OpCodes.Stfld, interfaceTypeField);
//调用基类的构造函数
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Call, objectConstructorInfo);
iLGenerator.Emit(OpCodes.Ret);
需要的东西都准备完了,剩下就是实现继承的接口的方法了。
在方法中,如果我们需要知道当前执行的方法的信息,可以使用 MethodBase.GetCurrentMethod,但是需要注意的是,此处获得的方法信息是这个实现类的,而不是接口的,所以我们需要事先将接口的方法信息存储起来之后,在这里获取。
首先定义存储类(此处给出部分代码,详情自行参照项目代码):
public static class EmitMetadataCache
{
private static ConcurrentDictionary<Type, Dictionary<String, MethodInfo>> _InterfaceMethodTables;
private static Object _SyncObject;
static EmitMetadataCache()
{
_SyncObject = new Object();
_InterfaceMethodTables = new ConcurrentDictionary<Type, Dictionary<String, MethodInfo>>();
}
public static MethodInfo GetMethodInfo(Type interfaceType, String methodSignName)
{
MethodInfo info = null;
if (_InterfaceMethodTables.TryGetValue(interfaceType, out var methodInfoTable))
{
if (methodInfoTable.ContainsKey(methodSignName))
{
info = methodInfoTable[methodSignName];
}
}
return info;
}
.....
之后再使用 Emit 再构建的方法中,获取到接口的方法信息
var cacheMethodInfo = typeof(EmitMetadataCache).GetMethod(
nameof(EmitMetadataCache.GetMethodInfo),
BindingFlags.Public | BindingFlags.Static);
//加载接口类型
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Ldfld, interfaceTypeField);
//加载接口方法对象
// EmitMetadataCache.GetMethodInfo(interfaceType,methodSignName);
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Ldfld, interfaceTypeField);
iLGenerator.Emit(OpCodes.Ldstr, methodSignName);
iLGenerator.Emit(OpCodes.Call, cacheMethodInfo);
回头我们再看处理接口的声明 Object InovkeHandle(Object proxy, Type interfaceType, MethodInfo invokeMethod, Object[] parameters);
这其中的第三个参数我们再上面解决了,第一个参数就是代理类对象本身,传入 this 即可,第二个参数传入存储接口类型的字段即可:
//加载 handler 对象
iLGenerator.Emit(OpCodes.Ldarg_0);
iLGenerator.Emit(OpCodes.Ldfld, handlerField);
//加载对象本身
iLGenerator.Emit(OpCodes.Ldarg_0);
现在还剩最后一个参数,参数对象列表,再当前方法中获取传入的参数对象列表,这在 C# 中是比较困难的事情,而在 IL 中可以简单使用 ldarg 指令(参见 .NET IL 指令集在Emit中的对应)加载传入的参数,先获方法的参数信息,之后循环获取参数对象:
iLGenerator.Emit(OpCodes.Ldc_I4, parameterCount);
iLGenerator.Emit(OpCodes.Newarr, typeof(Object));
iLGenerator.Emit(OpCodes.Stloc, parameterObjects.LocalIndex);
for (int k = 0; k < parameterCount; k++)
{
//推送数组引用到堆栈上
iLGenerator.Emit(OpCodes.Ldloc, parameterObjects.LocalIndex);
//将数组索引推送到堆栈上
iLGenerator.Emit(OpCodes.Ldc_I4, k);
//第一个参数为对象本身,所以该用 k+1
iLGenerator.Emit(OpCodes.Ldarg, k + 1);
if (parameterTypeInfos[k].IsValueType)
{
iLGenerator.Emit(OpCodes.Box, parameterTypeInfos[k]);
}
iLGenerator.Emit(OpCodes.Stelem_Ref);
}
iLGenerator.Emit(OpCodes.Ldloc, parameterObjects.LocalIndex);
到此所有处理接口所需的参数都准备完毕了,调用处理对象的处理方法即可,注意这些参数加载到栈顶的顺序应该是 proxy、interfaceType、methodInfo、parameters,而调用指令从栈顶获取参数填充到调用中的顺序却是 parameters、methodInfo、interfaceType、proxy ,原本我以为是从左至右的,特意将参数的加载顺序调反了,后面发现是从右到左的。
至此我们处理方法的调用准备工作以及调用都说明完了,但是调用之后的处理还需要注意一下,处理方法始终返回的是 Object 对象,针对于某个返回 值类型的接口方法声明时我们需要一个拆箱操作。而 void 返回类型的方法我们需要清除栈顶(pop 原因再前面说过)。
if (!methodInfo.ReturnType.Equals(typeof(void)))
{
if (methodInfo.ReturnType.IsValueType)
{
iLGenerator.Emit(OpCodes.Unbox, methodInfo.ReturnType);
if (methodInfo.ReturnType.IsEnum)
{
iLGenerator.Emit(OpCodes.Ldind_I4);
}
else if (!methodInfo.ReturnType.IsPrimitive)
{
iLGenerator.Emit(OpCodes.Ldobj, methodInfo.ReturnType);
}
else
{
iLGenerator.Emit(_TypeLdOpCodeTable[methodInfo.ReturnType]);
}
}
}
else
{
//如果方法本身没有返回值,此时应该清除栈顶的数据
iLGenerator.Emit(OpCodes.Pop);
}
iLGenerator.Emit(OpCodes.Ret);
最后,有可能我们实现的接口类型本身也继承了其他接口,这些继承的方法成员我们也应该实现
var parentInterfaces = interfaceType.GetInterfaces();
if (parentInterfaces.Length > 0)
{
foreach (var parentInterfaceType in parentInterfaces)
{
DynamicallyCreateProxyTypeMethod(parentInterfaceType, proxyTypeBuilder, handlerField, interfaceTypeField);
}
}
这么一来,我们的动态类型就完全构建完毕了,但是很明显,这种构建工作,如果每次创建新的接口代理对象时都重复一遍,效率是极为低下的,所以我们需要做一点操作将构建行为缓存起来:
//public DynamicProxy(IDynamicProxyHandler handler,Type interfaceType)
//Object proxy=(Object)(new ProxyType(handler,interfaceType));
var proxyTypeConstructor = proxyType.GetConstructor(new Type[] { typeof(IDynamicProxyHandler), typeof(Type) });
var handlerParameterExpression = Expression.Parameter(typeof(IDynamicProxyHandler));
var typeParameterExpression = Expression.Parameter(typeof(Type));
var newExpression = Expression.New(proxyTypeConstructor, new Expression[] {
handlerParameterExpression,typeParameterExpression
});
Expression castExpression = Expression.Convert(newExpression, typeof(Object));
proxyFunc = Expression.Lambda<Func<IDynamicProxyHandler, Type, Object>>(
newExpression,
handlerParameterExpression,
typeParameterExpression).Compile();
AddProxyCreateDelegateCache(interfaceType, proxyFunc);
此处我们使用表达式生成一个 Func<IDynamicProxyHandler, Type, Object> 类型的委托对象,此委托对象调用后返回我们构造的动态类的对象,将委托缓存起来,之后再构造相同接口类型的代理对象时,只需要调用此委托。
结尾处我们再看看调用过程:
var interfaceObj = ClientProxyFactory.CreateProxy<TestInterface>(
new DynamicProxyHandler()
);
interfaceObj.VoidTest();
interfaceObj.VoidTestParameters(56);
interfaceObj.RetTest();
interfaceObj.RetTestParameters(12, 63);
调用中传入了我们自行构造的处理对象,后续所有对接口方法的调用都会被转移至我们的处理对象,然后在处理方法中执行您已写好的代码。
正如文中说过的,这篇文章也是让你对拦截调用有一个了解、入门,具体深入还需要靠大家自己研究,全文简单几千字不可能说清楚每个细节,很多地方只是给您一个引导、一个启发,最关键的还是您对照代码、思路自行实现一遍,并加以扩展,才能感受到属于自己的东西。