基于.net core微服務(wù)的另一種實(shí)現(xiàn)方法
前言
基于.net core 的微服務(wù),網(wǎng)上很多介紹都是千篇一律基于類(lèi)似webapi,通過(guò)http請(qǐng)求形式進(jìn)行訪問(wèn),但這并不符合大家使用習(xí)慣.如何像形如[ GetService<IOrderService>().SaveOrder(orderInfo)]的方式, 調(diào)用遠(yuǎn)程的服務(wù),如果你正在為此苦惱, 本文或許是一種參考.
背景
原項(xiàng)目基于傳統(tǒng)三層模式組織代碼邏輯,隨著時(shí)間的推移,項(xiàng)目?jī)?nèi)各模塊邏輯互相交織,互相依賴(lài),維護(hù)起來(lái)較為困難.為此我們需要引入一種新的機(jī)制來(lái)嘗試改變這個(gè)現(xiàn)狀,在考察了 Java spring cloud/doubbo, c# wcf/webapi/asp.net core 等一些微服務(wù)框架后,我們最終選擇了基于 .net core + Ocelot 微服務(wù)方式. 經(jīng)過(guò)討論大家最終期望的項(xiàng)目結(jié)果大致如下所示.

但原項(xiàng)目團(tuán)隊(duì)成員已經(jīng)習(xí)慣了基于接口服務(wù)的這種編碼形式, 讓大家將需要定義的接口全部以http 接口形式重寫(xiě)定義一遍, 同時(shí)客戶(hù)端調(diào)用的時(shí)候, 需要將原來(lái)熟悉的形如 XXService.YYMethod(args1, args2) 直接使用通過(guò) "."出內(nèi)部成員,替換為讓其直接寫(xiě) HttpClient.Post("url/XX/YY",”args1=11&args2=22”)的形式訪問(wèn)遠(yuǎn)程接口,確實(shí)是一件十分痛苦的事情.
問(wèn)題提出
基于以上, 如何通過(guò)一種模式來(lái)簡(jiǎn)化這種調(diào)用形式, 繼而使大家在調(diào)用的時(shí)候不需要關(guān)心該服務(wù)是在本地(本地類(lèi)庫(kù)依賴(lài))還是遠(yuǎn)程, 只需要按照常規(guī)方式使用即可, 至于是直接使用本地服務(wù)還是通過(guò)http發(fā)送遠(yuǎn)程請(qǐng)求,這個(gè)都交給框架處理.為了方便敘述, 本文假定以銷(xiāo)售訂單和用戶(hù)服務(wù)為例. 銷(xiāo)售訂單服務(wù)對(duì)外提供一個(gè)創(chuàng)建訂單的接口.訂單創(chuàng)建成功后, 調(diào)用用戶(hù)服務(wù)更新用戶(hù)積分.UML參考如下.

問(wèn)題轉(zhuǎn)化
- 在客戶(hù)端,通過(guò)微服務(wù)對(duì)外公開(kāi)的接口,生成接口代理, 即將接口需要的信息[接口名/方法名及該方法需要的參數(shù)]包裝成http請(qǐng)求向遠(yuǎn)程服務(wù)發(fā)起請(qǐng)求.
- 在微服務(wù)http接入段, 我們可以定義一個(gè)統(tǒng)一的入口,當(dāng)服務(wù)端收到請(qǐng)求后,解析出接口名/方法名及參數(shù)信息,并創(chuàng)建對(duì)應(yīng)的實(shí)現(xiàn)類(lèi),從而執(zhí)行接口請(qǐng)求,并將返回值通過(guò)http返回給客戶(hù)端.
- 最后,客戶(hù)端通過(guò)類(lèi)似
AppRuntims.Instance.GetService<IOrderService>().SaveOrder(orderInfo)形式訪問(wèn)遠(yuǎn)程服務(wù)創(chuàng)建訂單. - 數(shù)據(jù)以json格式傳輸.
解決方案及實(shí)現(xiàn)
為了便于處理,我們定義了一個(gè)空接口IApiService,用來(lái)標(biāo)識(shí)服務(wù)接口.
遠(yuǎn)程服務(wù)客戶(hù)端代理
public class RemoteServiceProxy : IApiService
{
public string Address { get; set; } //服務(wù)地址private ApiActionResult PostHttpRequest(string interfaceId, string methodId, params object[] p)
{
ApiActionResult apiRetult = null;
using (var httpClient = new HttpClient())
{
var param = new ArrayList(); //包裝參數(shù)
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; //提交請(qǐng)求
if (!response.IsSuccessStatusCode)
{
AppRuntimes.Instance.Loger.Error($"httpRequest error:{url},statuscode:{response.StatusCode}");
throw new ICVIPException("網(wǎng)絡(luò)異?;蚍?wù)響應(yīng)失敗");
}
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 ?? "服務(wù)請(qǐng)求失敗");
}
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("數(shù)據(jù)轉(zhuǎn)化失敗", ex);
throw;
}
return rs;
}
//沒(méi)有返回值的代理
public void InvokeWithoutReturn(string interfaceId, string methodId, params object[] param)
{
PostHttpRequest(interfaceId, methodId, param);
}
}
遠(yuǎn)程服務(wù)端http接入段統(tǒng)一入口
[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))
{
//解析參數(shù)
param = JsonConvert.DeserializeObject<ArrayList>(reqParam);
}
//轉(zhuǎn)交本地服務(wù)處理中心處理
var data = LocalServiceExector.Exec(interfaceId, methodId, param);
result = ApiActionResult.Success(data);
}
catch BusinessException ex) //業(yè)務(wù)異常
{
result = ApiActionResult.Error(ex.Message);
}
catch (Exception ex)
{
//業(yè)務(wù)異常
if (ex.InnerException is BusinessException)
{
result = ApiActionResult.Error(ex.InnerException.Message);
}
else
{
AppRuntimes.Instance.Loger.Error($"調(diào)用服務(wù)發(fā)生異常{interfaceId}-{methodId},data:{reqParam}", ex);
result = ApiActionResult.Fail("服務(wù)發(fā)生異常");
}
}
finally
{
stopwatch.Stop();
AppRuntimes.Instance.Loger.Debug($"process client request end:api/svc/{interfaceId}/{methodId},耗時(shí)[ {stopwatch.ElapsedMilliseconds} ]毫秒");
}
//result.Message = AppRuntimes.Instance.GetCfgVal("ServerName") + " " + result.Message;
result.Message = result.Message;
return result;
}
本地服務(wù)中心通過(guò)接口名和方法名,找出具體的實(shí)現(xiàn)類(lèi)的方法,并使用傳遞的參數(shù)執(zhí)行,ps:因?yàn)樯婕暗椒瓷浍@取具體的方法,暫不支持相同參數(shù)個(gè)數(shù)的方法重載.僅支持不同參數(shù)個(gè)數(shù)的方法重載.
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接口的服務(wù)實(shí)現(xiàn):{interfaceId}");
}
var methods = svcType.GetMethods().Where(t => t.Name == methodId).ToList();
if (methods.IsNullEmpty())
{
throw new BusinessException($"在API接口[{interfaceId}]的服務(wù)實(shí)現(xiàn)中[{svcType.FullName}]找不到指定的方法:{methodId}");
}
var method = methods.FirstOrDefault(t => t.GetParameters().Length == paramCount);
if (method == null)
{
throw new ICVIPException($"在API接口中[{interfaceId}]的服務(wù)實(shí)現(xiàn)[{svcType.FullName}]中,方法[{methodId}]參數(shù)個(gè)數(shù)不匹配");
}
var paramtersTypes = method.GetParameters();
object instance = null;
try
{
instance = Activator.CreateInstance(svcType);
}
catch (Exception ex)
{
throw new BusinessException($"在實(shí)例化服務(wù)[{svcType}]發(fā)生異常,請(qǐng)確認(rèn)其是否包含一個(gè)無(wú)參的構(gòu)造函數(shù)", 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;
服務(wù)配置,指示具體的服務(wù)的遠(yuǎn)程地址,當(dāng)未配置的服務(wù)默認(rèn)為本地服務(wù).
[
{
"ServiceId": "XZL.Api.IOrderService",
"Address": "http://localhost:8801/api/svc"
},
{
"ServiceId": "XZL.Api.IUserService",
"Address": "http://localhost:8802/api/svc"
}
]
AppRuntime.Instance.GetService<TService>()的實(shí)現(xiàn).
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;
//讀取服務(wù)配置
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; //指定服務(wù)地址
}
return rs;
}
}
public static TService GetService<TService>(string interfaceId, bool isSingle)
{
//服務(wù)非單例模式
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($"系統(tǒng)中未找到[{interfaceId}]的代理類(lèi)");
}
obj = Activator.CreateInstance(svcType);
svcInstance.TryAdd(interfaceId, obj);
return (TService)obj;
}
//獲取服務(wù)的實(shí)現(xiàn)類(lèi)
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;
}
為了性能影響,我們?cè)诔绦騿?dòng)的時(shí)候可以將當(dāng)前所有的ApiService類(lèi)型緩存.
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實(shí)現(xiàn)
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)
{
//客戶(hù)端代理基類(lèi)
if (type == typeof(RemoteServiceProxy))
{
continue;
}
if (!typeof(IApiService).IsAssignableFrom(type))
{
continue;
}
//綁定現(xiàn)類(lèi)
lst.Add((type.FullName, type));
foreach (var interfaceType in type.GetInterfaces())
{
if (!typeof(IApiService).IsAssignableFrom(interfaceType))
{
continue;
}
//綁定接口與實(shí)際實(shí)現(xiàn)類(lèi)
lst.Add((interfaceType.FullName, type));
}
}
}
catch
{
}
return lst;
}
具體api遠(yuǎn)程服務(wù)代理示例
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);
}
}
結(jié)語(yǔ)
經(jīng)過(guò)以上改造后, 我們便可很方便的通過(guò)形如 AppRuntime.Instance.GetService<TService>().MethodXX()無(wú)感的訪問(wèn)遠(yuǎn)程服務(wù), 服務(wù)是部署在遠(yuǎn)程還是在本地以dll依賴(lài)形式存在,這個(gè)便對(duì)調(diào)用者透明了.無(wú)縫的對(duì)接上了大家固有習(xí)慣.
PS: 但是此番改造后, 遺留下來(lái)了另外一個(gè)問(wèn)題: 客戶(hù)端調(diào)用遠(yuǎn)程服務(wù),需要手動(dòng)創(chuàng)建一個(gè)服務(wù)代理( 從 RemoteServiceProxy 繼承),雖然每個(gè)代理很方便寫(xiě),只是文中提到的簡(jiǎn)單兩句話,但終究顯得繁瑣, 是否有一種方式能夠根據(jù)遠(yuǎn)程api接口動(dòng)態(tài)的生成這個(gè)客戶(hù)端代理呢? 答案是肯定的,因本文較長(zhǎng)了,留在下篇再續(xù)
附上動(dòng)態(tài)編譯文章鏈接:http://m.fzitv.net/article/144101.htm
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
擴(kuò)展了Repeater控件的EmptyDataTemplate模板功能
Repeater控件是一個(gè)數(shù)據(jù)顯示控件,該控件允許通過(guò)為列表中顯示的每一項(xiàng)重復(fù)使用指定的模板來(lái)自定義布局2013-01-01
asp.net ASPxTextBox等控件實(shí)現(xiàn)"回車(chē)模擬Tab"的 常用代碼整理
今天我要實(shí)現(xiàn)一些編輯框如ASPxTextBox、ASPxComboBox等控件回車(chē)模擬Tab的功能。這沒(méi)辦法,用戶(hù)用慣了回車(chē),討厭按Tab來(lái)移動(dòng)焦點(diǎn)(鼠標(biāo)點(diǎn)擊更麻煩)。2010-03-03
ASP.NET Core 使用Cookie驗(yàn)證身份的示例代碼
這篇文章主要介紹了ASP.NET Core 使用Cookie驗(yàn)證身份的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
asp.net 從POST的數(shù)據(jù)流中提取參數(shù)和文件
按理,F(xiàn)orm提交的數(shù)據(jù),無(wú)論是application/x-www-form-urlencoded還是multipart/form-data(有附件時(shí)),都可在服務(wù)端通過(guò)Request.Form["name"]和Request.Files["name"]獲取到參數(shù)和上傳的文件。2010-02-02
.net搜索查詢(xún)并實(shí)現(xiàn)分頁(yè)實(shí)例
.net搜索查詢(xún)并實(shí)現(xiàn)分頁(yè)實(shí)例,需要的朋友可以參考一下2013-03-03

