欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-21 15:16:17
前言 基于.net core 的微服务,网上很多介绍都是千篇一律基于类似webapi,通过http请求形式进行访问,但这并不符合大家使用习惯.如何像形如[ getserv...

前言

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

背景

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

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

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

问题提出

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

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

问题转化

  • 在客户端,通过微服务对外公开的接口,生成接口代理, 即将接口需要的信息[接口名/方法名及该方法需要的参数]包装成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接口动态的生成这个客户端代理呢? 答案是肯定的,因本文较长了,留在下篇再续

附上动态编译文章链接:

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