基于.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接口动态的生成这个客户端代理呢? 答案是肯定的,因本文较长了,留在下篇再续
附上动态编译文章链接:
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
推荐阅读
-
微信公共服务平台开发(.Net 的实现)4-------语音识别
-
一行代码在Linux服务器上搭建基于.Net Core的博客
-
.net core 实现基于 JSON 的多语言
-
微信公共服务平台开发(.Net 的实现)2-------获得ACCESSTOKEN
-
微信公共服务平台开发(.Net 的实现)3-------发送文本消息
-
微信公共服务平台开发(.Net 的实现)7-------发送图文消息
-
微信公共服务平台开发(.Net 的实现)6-------自定义菜单
-
ASP.NET Core 实战:基于 Dapper 扩展你的数据访问方法
-
.Net Core导入千万级数据至Mysql数据库的实现方法
-
asp.net core MVC之实现基于token的认证