这篇文章会详细阐述如何使用 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);
        }

最后就是调用的效果了:

img

接下来我们详细说明,如何在运行时动态构建程序集、模块、类以及其类成员。

首先以一小段简易的 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

*img

编译这段代码,并得到可执行文件,执行其运行结果如下:

img

  • .assembly extern 定义了称为 AssemblyRef 的元数据项,此元数据项目记录了当前程序集对某个外部程序集的引用,可以将 .assembly extern mscorlib 这段代码的作用暂时等价理解为您在 C# 项目中使用工具手动为项目添加名为 mscorlib.dll (一般会被系统自动引用 的程序集的引用。

    img

  • .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 示例一样的结果:

img

使用 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);

调用中传入了我们自行构造的处理对象,后续所有对接口方法的调用都会被转移至我们的处理对象,然后在处理方法中执行您已写好的代码。

正如文中说过的,这篇文章也是让你对拦截调用有一个了解、入门,具体深入还需要靠大家自己研究,全文简单几千字不可能说清楚每个细节,很多地方只是给您一个引导、一个启发,最关键的还是您对照代码、思路自行实现一遍,并加以扩展,才能感受到属于自己的东西。