前言
基于.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,用來標識服務接口.
遠程服務客戶端代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
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接入段統一入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
[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:因為涉及到反射獲取具體的方法,暫不支持相同參數個數的方法重載.僅支持不同參數個數的方法重載.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
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; |
服務配置,指示具體的服務的遠程地址,當未配置的服務默認為本地服務.
1
2
3
4
5
6
7
8
9
10
|
[ { "ServiceId": "XZL.Api.IOrderService", "Address": "http://localhost:8801/api/svc" }, { "ServiceId": "XZL.Api.IUserService", "Address": "http://localhost:8802/api/svc" } ] |
AppRuntime.Instance.GetService<TService>()
的實現.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
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類型緩存.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
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遠程服務代理示例
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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接口動態的生成這個客戶端代理呢? 答案是肯定的,因本文較長了,留在下篇再續
附上動態編譯文章鏈接:http://m.ythuaji.com.cn/article/70713.html
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:http://www.cnblogs.com/xie-zhonglai/p/netcore_micro_svc.html