视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
基于.net standard 的动态编译实现代码
2020-11-27 22:34:57 责编:小采
文档


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

1.背景:

其一在前文中,我们通过框架实现了微服务面向使用者的透明调用,但是需要为每个服务写一个客户端代理,显得异常繁琐,其二项目中前端站点使用了传统的.Net Framework 框架,后端微服务我们使用了.Net Core 框架改造,短时间将前端站点调整成 .Net Core 框架亦不现实,为了能同时支持这两种框架。如何 .Net Standard 框架来自动创建微服务的客户端代理成为我们必须解决的问题。

2.问题转化

我们在回头简单看一下我们现在期望的微服务客户端代理长的样子:

        通过上面分析,我们只需要将服务接口中的每个方法,判断是否有返回值,如果有返回值调用Invoke<ReturnType>方法,没有返回值调用InvokeWithoutReturn方法,然后依次将接口名,方法名以及方法的参数按顺序传入即可。各位如果是熟悉Java的同学,这个问题很容易解决,使用动态代理创建一个这样的匿名类即可,但在.net 的世界里,动态代理的实现确显得异常麻烦。
       首先想到是通过中间语言 IL 的 Emit 实现,但无奈这个使用起来实在是太不友好了, 几经折腾最终还是选择放弃了,后又想到其实可以通过动态生成这个代码片段,动态编译后加载到系统程序集中,应该就可以了。于是在这个方向的指引下,我们尝试着去一步步实现这个问题。

3.解决方案

如何生成这个代码片段? 通过上面的分析,我们知道只需要将接口反射获取其中的公共方法,并将接口的每个方法签名原样复制,在根据接口方法是否有返回值分别调用RemoteServiceProxy基类中相关方法即可,不过需要特殊注意的泛型方法翻译,以下是生成这个代码片段的参考实现.

寻找出为服务接口程序集文件,并处理每个文件

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;
}

处理每个文件中的接口类型,并将每个程序集的依赖程序集找出来,方便后面动态编译

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
 {
 }
 }

处理接口中的每个方法

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("}"); 
}

获取泛型类型字符串

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;
}

如何添加依赖

既然是要编译源码,那么源码中的依赖必不可少,在上一步中我们已经将每个程序集的依赖一并找出,接下来我们将这些依赖全部整理出来

//缓存程序集依赖
 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));
 }
 }

编译

有了代码片段, 也有了编译程序集依赖, 接下来就是最重要的编译了.

//定义编译后文件名
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);
 }
}

结语

在经过以上处理后,虽算不上完美,但顺利的实现了我们期望的样子,在之前的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;

总结

以上所述是小编给大家介绍的基于.net standard 的动态编译实现代码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

下载本文
显示全文
专题