zoukankan      html  css  js  c++  java
  • 基于.net standard 的动态编译实现

    在前文[基于.net core 微服务的另类实现]结尾处,提到了如何方便自动的生成微服务的客户端代理,使对于调用方透明,同时将枯燥的东西使用框架集成,以提高使用便捷性。在尝试了基于 Emit 中间语言后,最终决定使用生成代码片段然后动态编译的模式实现。

    1. 背景:
      其一在前文中,我们通过框架实现了微服务面向使用者的透明调用,但是需要为每个服务写一个客户端代理,显得异常繁琐,其二项目中前端站点使用了传统的.Net Framework 框架,后端微服务我们使用了.Net Core 框架改造,短时间将前端站点调整成 .Net Core 框架亦不现实,为了能同时支持这两种框架。如何 .Net Standard 框架来自动创建微服务的客户端代理成为我们必须解决的问题。
    2. 问题转化
      我们在回头简单看一下我们现在期望的微服务客户端代理长的样子:
      image
              通过上面分析,我们只需要将服务接口中的每个方法,判断是否有返回值,如果有返回值调用Invoke<ReturnType>方法,没有返回值调用InvokeWithoutReturn方法,然后依次将接口名,方法名以及方法的参数按顺序传入即可。各位如果是熟悉Java的同学,这个问题很容易解决,使用动态代理创建一个这样的匿名类即可,但在.net 的世界里,动态代理的实现确显得异常麻烦。
             首先想到是通过中间语言 IL 的 Emit 实现,但无奈这个使用起来实在是太不友好了, 几经折腾最终还是选择放弃了,后又想到其实可以通过动态生成这个代码片段,动态编译后加载到系统程序集中,应该就可以了。于是在这个方向的指引下,我们尝试着去一步步实现这个问题。
    3. 解决方案
      1. 如何生成这个代码片段? 通过上面的分析,我们知道只需要将接口反射获取其中的公共方法,并将接口的每个方法签名原样复制,在根据接口方法是否有返回值分别调用RemoteServiceProxy基类中相关方法即可,不过需要特殊注意的泛型方法翻译,以下是生成这个代码片段的参考实现.
        1. 寻找出为服务接口程序集文件,并处理每个文件

          private static StringBuilder CreateApiProxyCode()
          {
              var path = GetBinPath();
              var dir = new DirectoryInfo(path);
          
              //获取项目中微服务接口文件
              var files = dir.GetFiles("XZL*.Api.dll");
          
              var codeStringBuilder = new StringBuilder(1024);
          
              //添加必要的using
              codeStringBuilder
                  .AppendLine("using System;")
                  .AppendLine("using System.Collections.Generic;")
                  .AppendLine("using System.Text;")
                  .AppendLine("using XZL.Infrastructure.ApiService;")
                  .AppendLine("using XZL.Infrastructure.Defines;")
                  .AppendLine("using XZL.Model;")
                  .AppendLine("namespace XZL.ApiClientProxy")
                  .AppendLine("{");                  //namespace begin
          
              //处理每个文件中的接口信息
              foreach (var file in files)
              {
                  CreateApiProxyCodeFromFile(codeStringBuilder, file);
              }
          
              codeStringBuilder.AppendLine("}");      //namespace end
          
              return codeStringBuilder;
          }
        2. 处理每个文件中的接口类型,并将每个程序集的依赖程序集找出来,方便后面动态编译

          private static void CreateApiProxyCodeFromFile(StringBuilder fileCodeBuilder, FileInfo file)
           {
               try
               {
                   Assembly apiAssembly = Assembly.Load(file.Name.Substring(0, file.Name.Length - 4));
          
                   var types = apiAssembly
                                   .GetTypes()
                                   .Where(c => c.IsInterface && c.IsPublic)
                                   .ToList();
          
                   var apiSvcType = typeof(IApiService);
          
                   bool isNeed = false;
                   foreach (Type type in types)
                   {
                       //找出期望的接口类型
                       if (!apiSvcType.IsAssignableFrom(type))
                       {
                           continue;
                       }
          
                       //找出接口的所有方法
                       var methods = type.GetMethods(BindingFlags.Public 
                           | BindingFlags.FlattenHierarchy 
                           | BindingFlags.Instance);
          
                       if (!methods.Any())
                       {
                           continue;
                       }
                       //定义代理类名,以及实现接口和继承RemoteServiceProxy
                       fileCodeBuilder.AppendLine($"public class {type.FullName.Replace(".", "_")}Proxy :" +
                                           $"RemoteServiceProxy, {type.FullName}")
                                      .AppendLine("{");        //class begin
          
                       //处理每个方法
                       foreach (var mth in methods)
                       {
                           CreateApiProxyCodeFromMethod(fileCodeBuilder, type, mth);
                       }
                       fileCodeBuilder.AppendLine("}");        //class end
                       isNeed = true;
                   }
                   if (isNeed)
                   {
                       var apiRefAsms = apiAssembly.GetReferencedAssemblies();
                       refAssemblyList.Add(apiAssembly.GetName());
                       refAssemblyList.AddRange(apiRefAsms);
                   }
               }
               catch
               {
               }
           }
        3. 处理接口中的每个方法

          private static void CreateApiProxyCodeFromMethod(
                      StringBuilder fileCodeBuilder, 
                      Type type,
                      MethodInfo mth)
          {
              var isMthReturn = !mth.ReturnType.Equals(typeof(void));
          
              fileCodeBuilder.Append("public ");
          
              //添加返回值
              if (isMthReturn)
              {
                  fileCodeBuilder.Append(GetFriendlyTypeName(mth.ReturnType)).Append(" ");
              }
              else
              {
                  fileCodeBuilder.Append(" void ");
              }
          
              //方法参数开始
              fileCodeBuilder.Append(mth.Name).Append("(");       
          
              var mthParams = mth.GetParameters();
              if (mthParams.Any())
              {
                  var mthparaList = new List<string>();
                  foreach (var p in mthParams)
                  {
                      mthparaList.Add(GetFriendlyTypeName(p.ParameterType) + " " + p.Name);
                  }
                  fileCodeBuilder.Append(string.Join(",", mthparaList));
              }
          
              //方法参数结束
              fileCodeBuilder.Append(")");
          
              //方法体开始
              fileCodeBuilder.AppendLine("{");   
          
              if (isMthReturn)
              {
                  //返回值
                  fileCodeBuilder.Append("return Invoke<")
                                  .Append(GetFriendlyTypeName(mth.ReturnType))
                                  .Append(">");
              }
              else
              {
                  fileCodeBuilder.Append(" InvokeWithoutReturn");
              }
          
              //拼接接口名及方法名
              fileCodeBuilder.Append($"("{type.FullName}","{mth.Name}"");
          
              //方法本身参数
              if (mthParams.Any())
              {
                  fileCodeBuilder.Append(",").Append(string.Join(",", mthParams.Select(t => t.Name)));
              }
              fileCodeBuilder.Append(");");
          
              //方法体结束
              fileCodeBuilder.AppendLine("}");               
          }
        4. 获取泛型类型字符串

          private static string GetFriendlyTypeName(Type type)
          {
              if (!type.IsGenericType)
              {
                  return type.FullName;
              }
          
              string friendlyName = type.Name;
              int iBacktick = friendlyName.IndexOf('`');
              if (iBacktick > 0)
              {
                  friendlyName = friendlyName.Remove(iBacktick);
              }
              friendlyName += "<";
              Type[] typeParameters = type.GetGenericArguments();
              for (int i = 0; i < typeParameters.Length; ++i)
              {
                  string typeParamName = GetFriendlyTypeName(typeParameters[i]);
                  friendlyName += (i == 0 ? typeParamName : "," + typeParamName);
              }
              friendlyName += ">";
              return friendlyName;
          }
      2. 如何添加依赖
        既然是要编译源码,那么源码中的依赖必不可少,在上一步中我们已经将每个程序集的依赖一并找出,接下来我们将这些依赖全部整理出来

        //缓存程序集依赖
         var references = new List<MetadataReference>();     
         var refAsmFiles = new List<string>();
        
         //系统依赖
         var sysRefLocation = typeof(Enumerable).GetTypeInfo().Assembly.Location;
         refAsmFiles.Add(sysRefLocation);
        
         //refAsmFiles原本缓存的程序集依赖
         refAsmFiles.Add(typeof(object).GetTypeInfo().Assembly.Location);
         refAsmFiles.AddRange(refAssemblyList.Select(t => Assembly.Load(t).Location).Distinct().ToList());
        
         //传统.NetFramework 需要添加mscorlib.dll
         var coreDir = Directory.GetParent(sysRefLocation);
         var mscorlibFile = coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll";
         if (File.Exists(mscorlibFile))
         {
             references.Add(MetadataReference.CreateFromFile(mscorlibFile));
         }
        
         var apiAsms = refAsmFiles.Select(t => MetadataReference.CreateFromFile(t)).ToList();
         references.AddRange(apiAsms);
        
         //当前程序集依赖
         var thisAssembly = Assembly.GetEntryAssembly();
         if (thisAssembly != null)
         {
             var referencedAssemblies = thisAssembly.GetReferencedAssemblies();
             foreach (var referencedAssembly in referencedAssemblies)
             {
                 var loadedAssembly = Assembly.Load(referencedAssembly);
                 references.Add(MetadataReference.CreateFromFile(loadedAssembly.Location));
             }
         }
      3. 编译
        有了代码片段, 也有了编译程序集依赖, 接下来就是最重要的编译了.

        //定义编译后文件名
        var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Proxy");
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        var apiRemoteProxyDllFile = Path.Combine(path, 
            apiRemoteAsmName + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".dll");
        
        
        var tree = SyntaxFactory.ParseSyntaxTree(codeBuilder.ToString());
        var compilation = CSharpCompilation.Create(apiRemoteAsmName)
          .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
          .AddReferences(references)
          .AddSyntaxTrees(tree);
        
        //执行编译
        EmitResult compilationResult = compilation.Emit(apiRemoteProxyDllFile);
        if (compilationResult.Success)
        {
            // Load the assembly
            apiRemoteAsm = Assembly.LoadFrom(apiRemoteProxyDllFile);
        }
        else
        {
            foreach (Diagnostic codeIssue in compilationResult.Diagnostics)
            {
                string issue = $"ID: {codeIssue.Id}, Message: {codeIssue.GetMessage()}," +
                    $" Location: { codeIssue.Location.GetLineSpan()}, " +
                    $"Severity: { codeIssue.Severity}";
                AppRuntimes.Instance.Loger.Error("自动编译代码出现异常," + issue);
            }
        }
    4. 结语
      在经过以上处理后,虽算不上完美,但顺利的实现了我们期望的样子,在之前的GetService中,当发现属于远程服务的时候,只需要类似如下形式返回代理对象即可。同时为增加调用更加顺畅,我们将此编译的时机定在了发生在程序启动的时候,ps 当然或许还有一些其他更合适的时机.

      static ConcurrentDictionary<string, Object> svcInstance = new ConcurrentDictionary<string, object>();
      var typeName = "XZL.ApiClientProxy." + typeof(TService).FullName.Replace(".", "_") + "Proxy";
      
      
      object obj = null;
      if (svcInstance.TryGetValue(typeName, out obj) && obj != null)
      {
          return (TService)obj;
      }
      try
      {
          obj = (TService)apiRemoteAsm.CreateInstance(typeName);
          svcInstance.TryAdd(typeName, obj);
      }
      catch
      {
          throw new ICVIPException($"未找到 {typeof(TService).FullName} 的有效代理");
      }
      
      return (TService)obj;
  • 相关阅读:
    前端大文件分片上传/多线程上传
    网页大文件分片上传/多线程上传
    docker基础入门之二
    linux之iptable
    linux内核之网络协议栈
    ubuntu之iptables
    c++栈管理库TCMalloc、jeMalloc
    curl之post提交xml
    ceph基本操作整理
    docker基础入门之一
  • 原文地址:https://www.cnblogs.com/xie-zhonglai/p/dynamic_compilation_netstandard.html
Copyright © 2011-2022 走看看