基于.net core微服务的另一种实现方法

前言

基于.net core 的微服务,网上很多介绍都是千篇一律基于类似webapi,通过http请求形式进行访问,但这并不符合大家使用习惯.如何像形如[ GetService<IOrderService>().SaveOrder(orderInfo)]的方式, 调用远程的服务,如果你正在为此苦恼, 本文或许是一种参考.

背景

原项目基于传统三层模式组织代码逻辑,随着时间的推移,项目内各模块逻辑互相交织,互相依赖,维护起来较为困难.为此我们需要引入一种新的机制来尝试改变这个现状,在考察了 Java spring cloud/doubbo, c# wcf/webapi/asp.net core 等一些微服务框架后,我们最终选择了基于 .net core + Ocelot 微服务方式. 经过讨论大家最终期望的项目结果大致如下所示.

但原项目团队成员已经习惯了基于接口服务的这种编码形式, 让大家将需要定义的接口全部以http 接口形式重写定义一遍, 同时客户端调用的时候, 需要将原来熟悉的形如 XXService.YYMethod(args1, args2) 直接使用通过 "."出内部成员,替换为让其直接写 HttpClient.Post("url/XX/YY",”args1=11&args2=22”)的形式访问远程接口,确实是一件十分痛苦的事情.

问题提出

基于以上, 如何通过一种模式来简化这种调用形式, 继而使大家在调用的时候不需要关心该服务是在本地(本地类库依赖)还是远程, 只需要按照常规方式使用即可, 至于是直接使用本地服务还是通过http发送远程请求,这个都交给框架处理.为了方便叙述, 本文假定以销售订单和用户服务为例. 销售订单服务对外提供一个创建订单的接口.订单创建成功后, 调用用户服务更新用户积分.UML参考如下.

问题转化

  • 在客户端,通过微服务对外公开的接口,生成接口代理, 即将接口需要的信息[接口名/方法名及该方法需要的参数]包装成http请求向远程服务发起请求.
  • 在微服务http接入段, 我们可以定义一个统一的入口,当服务端收到请求后,解析出接口名/方法名及参数信息,并创建对应的实现类,从而执行接口请求,并将返回值通过http返回给客户端.
  • 最后,客户端通过类似 AppRuntims.Instance.GetService<IOrderService>().SaveOrder(orderInfo) 形式访问远程服务创建订单.
  • 数据以json格式传输.

解决方案及实现

为了便于处理,我们定义了一个空接口IApiService,用来标识服务接口.

远程服务客户端代理

public class RemoteServiceProxy : IApiService
{
 public string Address { get; set; } //服务地址private ApiActionResult PostHttpRequest(string interfaceId, string methodId, params object[] p)
 {
 ApiActionResult apiRetult = null;
 using (var httpClient = new HttpClient())
 {
  var param = new ArrayList(); //包装参数

  foreach (var t in p)
  {
  if (t == null)
  {
   param.Add(null);
  }
  else
  {
   var ns = t.GetType().Namespace;
   param.Add(ns != null && ns.Equals("System") ? t : JsonConvert.SerializeObject(t));
  }
  }
  var postContentStr = JsonConvert.SerializeObject(param);
  HttpContent httpContent = new StringContent(postContentStr);
  if (CurrentUserId != Guid.Empty)
  {
  httpContent.Headers.Add("UserId", CurrentUserId.ToString());
  }
  httpContent.Headers.Add("EnterpriseId", EnterpriseId.ToString());
  httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");

  var url = Address.TrimEnd('/') + $"/{interfaceId}/{methodId}";
  AppRuntimes.Instance.Loger.Debug($"httpRequest:{url},data:{postContentStr}");

  var response = httpClient.PostAsync(url, httpContent).Result; //提交请求

  if (!response.IsSuccessStatusCode)
  {
  AppRuntimes.Instance.Loger.Error($"httpRequest error:{url},statuscode:{response.StatusCode}");
  throw new ICVIPException("网络异常或服务响应失败");
  }
  var responseStr = response.Content.ReadAsStringAsync().Result;
  AppRuntimes.Instance.Loger.Debug($"httpRequest response:{responseStr}");

  apiRetult = JsonConvert.DeserializeObject<ApiActionResult>(responseStr);
 }
 if (!apiRetult.IsSuccess)
 {
  throw new BusinessException(apiRetult.Message ?? "服务请求失败");
 }
 return apiRetult;
 }

 //有返回值的方法代理
 public T Invoke<T>(string interfaceId, string methodId, params object[] param)
 {
 T rs = default(T);

 var apiRetult = PostHttpRequest(interfaceId, methodId, param);

 try
 {
  if (typeof(T).Namespace == "System")
  {
  rs = (T)TypeConvertUtil.BasicTypeConvert(typeof(T), apiRetult.Data);
  }
  else
  {
  rs = JsonConvert.DeserializeObject<T>(Convert.ToString(apiRetult.Data));
  }
 }
 catch (Exception ex)
 {
  AppRuntimes.Instance.Loger.Error("数据转化失败", ex);
  throw;
 }
 return rs;
 }

 //没有返回值的代理
 public void InvokeWithoutReturn(string interfaceId, string methodId, params object[] param)
 {
 PostHttpRequest(interfaceId, methodId, param);
 }
}

远程服务端http接入段统一入口

[Route("api/svc/{interfaceId}/{methodId}"), Produces("application/json")]
public async Task<ApiActionResult> Process(string interfaceId, string methodId)
{
 Stopwatch stopwatch = new Stopwatch();
 stopwatch.Start();
 ApiActionResult result = null;
 string reqParam = string.Empty;
 try
 {
 using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
 {
  reqParam = await reader.ReadToEndAsync();
 }
 AppRuntimes.Instance.Loger.Debug($"recive client request:api/svc/{interfaceId}/{methodId},data:{reqParam}");

 ArrayList param = null;
 if (!string.IsNullOrWhiteSpace(reqParam))
 {
  //解析参数
  param = JsonConvert.DeserializeObject<ArrayList>(reqParam);
 }
 //转交本地服务处理中心处理
 var data = LocalServiceExector.Exec(interfaceId, methodId, param);
 result = ApiActionResult.Success(data);
 }
 catch BusinessException ex) //业务异常
 {
 result = ApiActionResult.Error(ex.Message);
 }
 catch (Exception ex)
 {
 //业务异常
 if (ex.InnerException is BusinessException)
 {
  result = ApiActionResult.Error(ex.InnerException.Message);
 }
 else
 {
  AppRuntimes.Instance.Loger.Error($"调用服务发生异常{interfaceId}-{methodId},data:{reqParam}", ex);
  result = ApiActionResult.Fail("服务发生异常");
 }
 }
 finally
 {
 stopwatch.Stop();
 AppRuntimes.Instance.Loger.Debug($"process client request end:api/svc/{interfaceId}/{methodId},耗时[ {stopwatch.ElapsedMilliseconds} ]毫秒");
 }
 //result.Message = AppRuntimes.Instance.GetCfgVal("ServerName") + " " + result.Message;
 result.Message = result.Message;
 return result;
}

本地服务中心通过接口名和方法名,找出具体的实现类的方法,并使用传递的参数执行,ps:因为涉及到反射获取具体的方法,暂不支持相同参数个数的方法重载.仅支持不同参数个数的方法重载.

public static object Exec(string interfaceId, string methodId, ArrayList param)
{
 var svcMethodInfo = GetInstanceAndMethod(interfaceId, methodId, param.Count);
 var currentMethodParameters = new ArrayList();

 for (var i = 0; i < svcMethodInfo.Paramters.Length; i++)
 {
 var tempParamter = svcMethodInfo.Paramters[i];

 if (param[i] == null)
 {
  currentMethodParameters.Add(null);
 }
 else
 {
  if (!tempParamter.ParameterType.Namespace.Equals("System") || tempParamter.ParameterType.Name == "Byte[]")
  {
  currentMethodParameters.Add(JsonConvert.DeserializeObject(Convert.ToString(param[i]), tempParamter.ParameterType)
  }
  else
  {
  currentMethodParameters.Add(TypeConvertUtil.BasicTypeConvert(tempParamter.ParameterType, param[i]));
  }
 }
 }

 return svcMethodInfo.Invoke(currentMethodParameters.ToArray());
}

private static InstanceMethodInfo GetInstanceAndMethod(string interfaceId, string methodId, int paramCount)
{
 var methodKey = $"{interfaceId}_{methodId}_{paramCount}";
 if (methodCache.ContainsKey(methodKey))
 {
 return methodCache[methodKey];
 }
 InstanceMethodInfo temp = null;

 var svcType = ServiceFactory.GetSvcType(interfaceId, true);
 if (svcType == null)
 {
 throw new ICVIPException($"找不到API接口的服务实现:{interfaceId}");
 }
 var methods = svcType.GetMethods().Where(t => t.Name == methodId).ToList();
 if (methods.IsNullEmpty())
 {
 throw new BusinessException($"在API接口[{interfaceId}]的服务实现中[{svcType.FullName}]找不到指定的方法:{methodId}");
 }
 var method = methods.FirstOrDefault(t => t.GetParameters().Length == paramCount);
 if (method == null)
 {
 throw new ICVIPException($"在API接口中[{interfaceId}]的服务实现[{svcType.FullName}]中,方法[{methodId}]参数个数不匹配");
 }
 var paramtersTypes = method.GetParameters();

 object instance = null;
 try
 {
 instance = Activator.CreateInstance(svcType);
 }
 catch (Exception ex)
 {
 throw new BusinessException($"在实例化服务[{svcType}]发生异常,请确认其是否包含一个无参的构造函数", ex);
 }
 temp = new InstanceMethodInfo()
 {
 Instance = instance,
 InstanceType = svcType,
 Key = methodKey,
 Method = method,
 Paramters = paramtersTypes
 };
 if (!methodCache.ContainsKey(methodKey))
 {
 lock (_syncAddMethodCacheLocker)
 {
  if (!methodCache.ContainsKey(methodKey))
  {
  methodCache.Add(methodKey, temp);
  }
 }
 }
 return temp;

服务配置,指示具体的服务的远程地址,当未配置的服务默认为本地服务.

[
 {
 "ServiceId": "XZL.Api.IOrderService",
 "Address": "http://localhost:8801/api/svc"
 },
 {
 "ServiceId": "XZL.Api.IUserService",
 "Address": "http://localhost:8802/api/svc"
 }
]

AppRuntime.Instance.GetService<TService>()的实现.

private static List<(string typeName, Type svcType)> svcTypeDic;
private static ConcurrentDictionary<string, Object> svcInstance = new ConcurrentDictionary<string, object>();

public static TService GetService<TService>()
 {
 var serviceId = typeof(TService).FullName;

 //读取服务配置
 var serviceInfo = ServiceConfonfig.Instance.GetServiceInfo(serviceId);
 if (serviceInfo == null)
 {
  return (TService)Activator.CreateInstance(GetSvcType(serviceId));
 }
 else
 {
  var rs = GetService<TService>(serviceId + (serviceInfo.IsRemote ? "|Remote" : ""), serviceInfo.IsSingle);
  if (rs != null && rs is RemoteServiceProxy)
  {
  var temp = rs as RemoteServiceProxy;
  temp.Address = serviceInfo.Address; //指定服务地址
  }
  return rs;
 }
 }
public static TService GetService<TService>(string interfaceId, bool isSingle)
{
 //服务非单例模式
 if (!isSingle)
 {
 return (TService)Activator.CreateInstance(GetSvcType(interfaceId));
 }

 object obj = null;
 if (svcInstance.TryGetValue(interfaceId, out obj) && obj != null)
 {
 return (TService)obj;
 }
 var svcType = GetSvcType(interfaceId);

 if (svcType == null)
 {
 throw new ICVIPException($"系统中未找到[{interfaceId}]的代理类");
 }
 obj = Activator.CreateInstance(svcType);

 svcInstance.TryAdd(interfaceId, obj);
 return (TService)obj;
}

//获取服务的实现类
public static Type GetSvcType(string interfaceId, bool? isLocal = null)
{
 if (!_loaded)
 {
 LoadServiceType();
 }
 Type rs = null;
 var tempKey = interfaceId;

 var temp = svcTypeDic.Where(x => x.typeName == tempKey).ToList();

 if (temp == null || temp.Count == 0)
 {
 return rs;
 }

 if (isLocal.HasValue)
 {
 if (isLocal.Value)
 {
  rs = temp.FirstOrDefault(t => !typeof(RemoteServiceProxy).IsAssignableFrom(t.svcType)).svcType;
 }
 else
 {
  rs = temp.FirstOrDefault(t => typeof(RemoteServiceProxy).IsAssignableFrom(t.svcType)).svcType;
 }
 }
 else
 {
 rs = temp[0].svcType;
 }
 return rs;
}

为了性能影响,我们在程序启动的时候可以将当前所有的ApiService类型缓存.

public static void LoadServiceType()
 {
 if (_loaded)
 {
  return;
 }
 lock (_sync)
 {
  if (_loaded)
  {
  return;
  }
  try
  {
  svcTypeDic = new List<(string typeName, Type svcType)>();
  var path = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
  var dir = new DirectoryInfo(path);
  var files = dir.GetFiles("XZL*.dll");
  foreach (var file in files)
  {
   var types = LoadAssemblyFromFile(file);
   svcTypeDic.AddRange(types);
  }
  _loaded = true;
  }
  catch
  {
  _loaded = false;
  }
 }
 }

//加载指定文件中的ApiService实现
private static List<(string typeName, Type svcType)> LoadAssemblyFromFile(FileInfo file)
{
 var lst = new List<(string typeName, Type svcType)>();
 if (file.Extension != ".dll" && file.Extension != ".exe")
 {
 return lst;
 }
 try
 {
 var types = Assembly.Load(file.Name.Substring(0, file.Name.Length - 4))
   .GetTypes()
   .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic);
 foreach (Type type in types)
 {
  //客户端代理基类
  if (type == typeof(RemoteServiceProxy))
  {
  continue;
  }

  if (!typeof(IApiService).IsAssignableFrom(type))
  {
  continue;
  }

  //绑定现类
  lst.Add((type.FullName, type));

  foreach (var interfaceType in type.GetInterfaces())
  {
  if (!typeof(IApiService).IsAssignableFrom(interfaceType))
  {
   continue;
  }
 //绑定接口与实际实现类
  lst.Add((interfaceType.FullName, type));
  }
 }
 }
 catch
 {
 }

 return lst;
}

具体api远程服务代理示例

public class UserServiceProxy : RemoteServiceProxy, IUserService
 {
 private string serviceId = typeof(IUserService).FullName;

 public void IncreaseScore(int userId,int score)
 {
  return InvokeWithoutReturn(serviceId, nameof(IncreaseScore), userId,score);
 }
 public UserInfo GetUserById(int userId)
 {
  return Invoke<UserInfo >(serviceId, nameof(GetUserById), userId);
 }
}

结语

经过以上改造后, 我们便可很方便的通过形如 AppRuntime.Instance.GetService<TService>().MethodXX()无感的访问远程服务, 服务是部署在远程还是在本地以dll依赖形式存在,这个便对调用者透明了.无缝的对接上了大家固有习惯.

PS: 但是此番改造后, 遗留下来了另外一个问题: 客户端调用远程服务,需要手动创建一个服务代理( 从 RemoteServiceProxy 继承),虽然每个代理很方便写,只是文中提到的简单两句话,但终究显得繁琐, 是否有一种方式能够根据远程api接口动态的生成这个客户端代理呢? 答案是肯定的,因本文较长了,留在下篇再续

附上动态编译文章链接:https://www.jb51.net/article/144101.htm

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Asp.Net Core中服务的生命周期选项区别与用法详解

    前言 最近在做一个小的Demo中,在一个界面上两次调用视图组件,并且在视图组件中都调用了数据库查询,结果发现,一直报错,将两个视图组件的调用分离,单独进行,却又是正常的,寻找一番,发现是配置依赖注入服务时,对于服务的生命周期没有配置得当导致,特此做一次实验来认识三者之间(甚至是四者之间的用法及区别). 本文demo地址(具体见WebApi控制器中):https://gitee.com/530521314/koInstance.git (本地下载)  一.服务的生命周期 在Asp.Net Core

  • c# .Net Core静态文件服务器的新人入门教程

    概要: 本文通过示例,讲解了 NET Core2.0 静态文件目录的相关知识,并附带解析,适合新手,并附带了完整的项目代码.(项目通过 vs2017 初始化的 ASP.NET Core 应用程序,之后选择***空项目***) 示例代码 项目结构 program.cs文件 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks;

  • 云服务器下搭建ASP.NET Core环境

    最近.net core如火如荼,国内这方面环境搭建方面的文档也非常多,但是不少已经是过时的,就算按照那个流程走下去也避免不了一些地方早就不一样了.所以下面我将从头到尾的教大家搭建一次环境,并且成功运行官网的demo. 一.系统环境 本次笔者因为懒的去做虚拟机,所以注册了一个云提供商的试用账户作为本次的主机. 系统: Ubuntu Server 14.04.2 LTS 64bit Mono: 1.0.0-rc1-update1 Coreclr: 1.0.0-rc1-update1 二.正文 1.首

  • 详解.NET Core+Docker 开发微服务

    .NET Core发布很久了,因为近几年主要使用java,所以还没使用过.NET Core,今天正好有一个c#写的demo,需要做成服务,不想再转成java来实现,考虑使用.NET CORE来尝下鲜,目标是开发一个微服务,然后部署到Docker swarm集群,供其他应用调用. 环境准备 下载.NET core的最新版本2.1.3, 安装后打开命令行验证: 出现下面的输出,就代表安装成功了 dotnet Usage: dotnet [options] Usage: dotnet [path-to

  • 详解ASP.NET Core 网站发布到Linux服务器

    长期以来,使用.NET开发的应用只能运行在Windows平台上面,而目前国内蓬勃发展的互联网公司由于成本的考虑,大量使用免费的Linux平台,这就使得.NET空有一身绝技但无法得到广大的施展空间,.NET平台被认为只适合开发企业内部应用系统. 2016年6月27日,微软正式发布.NET Core 1.0.ASP.NET 1.0和Entity Framework Core 1.0,通吃 Windows.OS X和Linux三大操作系统..NET Core作为新一代跨平台.开源的.NET平台备受瞩目

  • 基于.net core微服务的另一种实现方法

    前言 基于.net core 的微服务,网上很多介绍都是千篇一律基于类似webapi,通过http请求形式进行访问,但这并不符合大家使用习惯.如何像形如[ GetService<IOrderService>().SaveOrder(orderInfo)]的方式, 调用远程的服务,如果你正在为此苦恼, 本文或许是一种参考. 背景 原项目基于传统三层模式组织代码逻辑,随着时间的推移,项目内各模块逻辑互相交织,互相依赖,维护起来较为困难.为此我们需要引入一种新的机制来尝试改变这个现状,在考察了 Ja

  • .Net Core微服务网关Ocelot基础介绍及集成

    网关是什么 简单来说,网关就是暴露给外部的请求入口.就和门卫一样,外面的人想要进来,必须要经过门卫.当然,网关并不一定是必须的,后端服务通过http也可以很好的向客户端提供服务.但是对于业务复杂.规模庞大的项目来说,使用网关有很多无法舍弃的好处,比如可以进行统一的请求聚合来节省流量.降低耦合度,可以赋予项目熔断限流的能力提高可用性等等. ocelot是什么 ocelot是.net core实现的开源的api网关项目,开源地址:https://github.com/ThreeMammals/Oce

  • .Net Core微服务rpc框架GRPC通信实际运用

    序 上一篇博客把grpc的概念说了个大概,介绍了proto的数据类型,基本语法,也写了个小demo,是不是没那么难? 今天要从理论到实际,写两个微服务,并利用grpc完成两者之间的通信.只是作为demo写的话会十分简单,毕竟理解为主. 服务端 首先要拿出之前写好的proto文件,然后修改两个属性: Build Action => Protobuf compiler gRpc Stub Classes => Server only 如图: 当然也可以在项目文件里看到它: 然后重新生成项目 ,会自

  • .Net Core微服务rpc框架GRPC通信基础

    什么是rpc? rpc,全称Remote Procedure Call,通过它,你可以像调用本地方法一样调用远程服务.前端可以调用后端方法,后端也可以调用前端方法.其实这个概念并不陌生,上一篇关于web实时应用的文章也提到了这个概念,并实现了前后端互调的操作. 在正式开始使用grpc之前,我们还需要掌握protocol buffer的概念. 什么是grpc? grpc来自大名鼎鼎的谷歌,孵化于CNCF基金会(docker.k8s同样出自这个基金会).它是一款高性能.开源.通用的rpc框架,你可以

  • 基于SpringCloudGateway实现微服务网关的方式

    目录 (一)什么是微服务网关 (二)Spring Cloud Gateway网关 2.1 核心概念: 2.2 搭建环境: (三) 路由配置详解 3.1 自定义断言配置 3.2 断言不匹配404页面自定义 (四)Spring Cloud Gateway过滤器 (五) 网关限流 5.1 常见的一些限流算法: 5.2 集成Sentinel进行限流 5.3 网关实现跨域 (六)总结 (一)什么是微服务网关 后端写完所有的微服务之后,最终是要交给前端去调用.我们都知道每个微服务都有各自的端口号,如果前端直

  • .Net Core微服务网关Ocelot集成Consul

    有consul基础的都知道,consul可以发现新增的服务,剔除掉无效的服务,赋予应用自动伸缩的能力.而ocelot如果集成了consul,那ocelot也能拥有这些能力,还可以自主选择负载均衡策略,灵活性更强. (建议看完前一篇文章再来实践这一篇,不然可能有难度) 上干货. 首先打开上一篇新建好的项目,继续添加nuget包: 然后注册相关服务: public void ConfigureServices(IServiceCollection services) { services.AddOc

  • .Net Core微服务网关Ocelot集成Consul

    有consul基础的都知道,consul可以发现新增的服务,剔除掉无效的服务,赋予应用自动伸缩的能力.而ocelot如果集成了consul,那ocelot也能拥有这些能力,还可以自主选择负载均衡策略,灵活性更强. (建议看完前一篇文章再来实践这一篇,不然可能有难度) 上干货. 首先打开上一篇新建好的项目,继续添加nuget包: 然后注册相关服务: public void ConfigureServices(IServiceCollection services) { services.AddOc

  • 基于.NET Core 3.1 网站开发和部署的方法

    一.准备开发环境 1.主要开发工具的选择 vscode .NET Core command-line interface (CLI) tools Dbeaver 这里选择vscode + .net core cli 是因为不管在Windows还是Linux和Mac上都能使用这一套工具,而且命令行工具也非常强大. 2.vscode安装C#插件 在vscode插件市场中搜索安装即可 新手还可以去这里了解vscode的强大之处 3.安装数据库 这里使用Centos7,因为.NET Core 3.1只支

  • 教你在Spring Boot微服务中集成gRPC通讯的方法

    一.首先声明gRPC接口 这里引入的是最新的gRpc-core 1.37版本, 采用的grcp-spring-boot-starter封装的版本进行实现,github地址: https://github.com/yidongnan/grpc-spring-boot-starter 要实现gRpc通讯, 先定义接口以及入参出参信息 syntax = "proto3"; option java_multiple_files = true; option java_package = &qu

  •  ASP.NET Core 模型验证过滤器的两种实现方法

    目录 第一种方法:.Net Core 禁用模型验证过滤器 第二种方法:自动替换默认模型验证 在.Net Core的时代中,框架会帮你自动验证model的state,也就是ModelState.框架会为你自动注册ModelStateInvalidFilter,这个会运行在OnActionExecuting事件里面. 基于现有框架的代码编写的话,所以我们不再需要在业务中耦合这样的模型判断代码,系统内部会检查ModelState是否为Valid,如果为InValid会直接返回400 BadReques

随机推荐