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

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

問題轉(zhuǎn)化
- 在客戶端,通過微服務(wù)對外公開的接口,生成接口代理, 即將接口需要的信息[接口名/方法名及該方法需要的參數(shù)]包裝成http請求向遠程服務(wù)發(fā)起請求.
- 在微服務(wù)http接入段, 我們可以定義一個統(tǒng)一的入口,當(dāng)服務(wù)端收到請求后,解析出接口名/方法名及參數(shù)信息,并創(chuàng)建對應(yīng)的實現(xiàn)類,從而執(zhí)行接口請求,并將返回值通過http返回給客戶端.
- 最后,客戶端通過類似
AppRuntims.Instance.GetService<IOrderService>().SaveOrder(orderInfo)形式訪問遠程服務(wù)創(chuàng)建訂單. - 數(shù)據(jù)以json格式傳輸.
解決方案及實現(xiàn)
為了便于處理,我們定義了一個空接口IApiService,用來標(biāo)識服務(wù)接口.
遠程服務(wù)客戶端代理
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; //提交請求
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ù)請求失敗");
}
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;
}
//沒有返回值的代理
public void InvokeWithoutReturn(string interfaceId, string methodId, params object[] param)
{
PostHttpRequest(interfaceId, methodId, param);
}
}
遠程服務(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},耗時[ {stopwatch.ElapsedMilliseconds} ]毫秒");
}
//result.Message = AppRuntimes.Instance.GetCfgVal("ServerName") + " " + result.Message;
result.Message = result.Message;
return result;
}
本地服務(wù)中心通過接口名和方法名,找出具體的實現(xiàn)類的方法,并使用傳遞的參數(shù)執(zhí)行,ps:因為涉及到反射獲取具體的方法,暫不支持相同參數(shù)個數(shù)的方法重載.僅支持不同參數(shù)個數(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ù)實現(xiàn):{interfaceId}");
}
var methods = svcType.GetMethods().Where(t => t.Name == methodId).ToList();
if (methods.IsNullEmpty())
{
throw new BusinessException($"在API接口[{interfaceId}]的服務(wù)實現(xiàn)中[{svcType.FullName}]找不到指定的方法:{methodId}");
}
var method = methods.FirstOrDefault(t => t.GetParameters().Length == paramCount);
if (method == null)
{
throw new ICVIPException($"在API接口中[{interfaceId}]的服務(wù)實現(xiàn)[{svcType.FullName}]中,方法[{methodId}]參數(shù)個數(shù)不匹配");
}
var paramtersTypes = method.GetParameters();
object instance = null;
try
{
instance = Activator.CreateInstance(svcType);
}
catch (Exception ex)
{
throw new BusinessException($"在實例化服務(wù)[{svcType}]發(fā)生異常,請確認其是否包含一個無參的構(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ù)的遠程地址,當(dāng)未配置的服務(wù)默認為本地服務(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>()的實現(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}]的代理類");
}
obj = Activator.CreateInstance(svcType);
svcInstance.TryAdd(interfaceId, obj);
return (TService)obj;
}
//獲取服務(wù)的實現(xiàn)類
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;
}
為了性能影響,我們在程序啟動的時候可以將當(dāng)前所有的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實現(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)
{
//客戶端代理基類
if (type == typeof(RemoteServiceProxy))
{
continue;
}
if (!typeof(IApiService).IsAssignableFrom(type))
{
continue;
}
//綁定現(xiàn)類
lst.Add((type.FullName, type));
foreach (var interfaceType in type.GetInterfaces())
{
if (!typeof(IApiService).IsAssignableFrom(interfaceType))
{
continue;
}
//綁定接口與實際實現(xiàn)類
lst.Add((interfaceType.FullName, type));
}
}
}
catch
{
}
return lst;
}
具體api遠程服務(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é)語
經(jīng)過以上改造后, 我們便可很方便的通過形如 AppRuntime.Instance.GetService<TService>().MethodXX()無感的訪問遠程服務(wù), 服務(wù)是部署在遠程還是在本地以dll依賴形式存在,這個便對調(diào)用者透明了.無縫的對接上了大家固有習(xí)慣.
PS: 但是此番改造后, 遺留下來了另外一個問題: 客戶端調(diào)用遠程服務(wù),需要手動創(chuàng)建一個服務(wù)代理( 從 RemoteServiceProxy 繼承),雖然每個代理很方便寫,只是文中提到的簡單兩句話,但終究顯得繁瑣, 是否有一種方式能夠根據(jù)遠程api接口動態(tài)的生成這個客戶端代理呢? 答案是肯定的,因本文較長了,留在下篇再續(xù)
附上動態(tài)編譯文章鏈接:http://www.dbjr.com.cn/article/144101.htm
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
ASP.NET Eval進行數(shù)據(jù)綁定的方法
ASP.NET Eval在數(shù)據(jù)綁定方面的應(yīng)用是眾所周知的,不過技術(shù)在發(fā)展,當(dāng)ASP.NET Eval 1.1變成ASP.NET Eval 2.0的時候,在操作的時候會有什么變化呢?2013-04-04
高效的使用 Response.Redirect解決一些不必要的問題
這篇文章主要介紹了如何高效的使用 Response.Redirect解決一些不必要的問題,需要的朋友可以參考下2014-05-05
asp.net遍歷文件夾下所有子文件夾并綁定到gridview上的方法
這篇文章主要介紹了asp.net遍歷文件夾下所有子文件夾并且遍歷配置文件某一節(jié)點中所有key,value并且綁定到GridView上,需要的朋友可以參考下2014-08-08

