基於.net core 微服務的另類實現

来源:https://www.cnblogs.com/xie-zhonglai/archive/2018/07/10/netcore_micro_svc.html
-Advertisement-
Play Games

基於.net core 的微服務,網上很多介紹都是千篇一律基於類似webapi,通過http請求形式進行訪問,但這並不符合大家使用習慣.如何像形如[ GetService<IOrderService>().SaveOrder(orderInfo)]的方式, 調用遠程的服務,如果你正在為此苦惱, 本文 ...


基於.net core 的微服務,網上很多介紹都是千篇一律基於類似webapi,通過http請求形式進行訪問,但這並不符合大家使用習慣.如何像形如[ GetService<IOrderService>().SaveOrder(orderInfo)]的方式, 調用遠程的服務,如果你正在為此苦惱, 本文或許是一種參考.

  1. 背景

            原項目基於傳統三層模式組織代碼邏輯,隨著時間的推移,項目內各模塊邏輯互相交織,互相依賴,維護起來較為困難.為此我們需要引入一種新的機制來嘗試改變這個現狀,在考察了 Java spring cloud/doubbo, c# wcf/webapi/asp.net core 等一些微服務框架後,我們最終選擇了基於 .net core + Ocelot 微服務方式. 經過討論大家最終期望的項目結果大致如下所示.


    image

          但原項目團隊成員已經習慣了基於介面服務的這種編碼形式, 讓大家將需要定義的介面全部以http 介面形式重寫定義一遍, 同時客戶端調用的時候, 需要將原來熟悉的形如 XXService.YYMethod(args1, args2) 直接使用通過 "."出內部成員,替換為讓其直接寫 HttpClient.Post("url/XX/YY",”args1=11&args2=22”)的形式訪問遠程介面,確實是一件十分痛苦的事情.

  2. 問題提出


         基於以上, 如何通過一種模式來簡化這種調用形式, 繼而使大家在調用的時候不需要關心該服務是在本地(本地類庫依賴)還是遠程, 只需要按照常規方式使用即可, 至於是直接使用本地服務還是通過http發送遠程請求,這個都交給框架處理.為了方便敘述, 本文假定以銷售訂單和用戶服務為例. 銷售訂單服務對外提供一個創建訂單的介面.訂單創建成功後, 調用用戶服務更新用戶積分.UML參考如下.
    image
  3. 問題轉化

    1. 在客戶端,通過微服務對外公開的介面,生成介面代理, 即將介面需要的信息[介面名/方法名及該方法需要的參數]包裝成http請求向遠程服務發起請求.
    2. 在微服務http接入段, 我們可以定義一個統一的入口,當服務端收到請求後,解析出介面名/方法名及參數信息,並創建對應的實現類,從而執行介面請求,並將返回值通過http返回給客戶端.
    3. 最後,客戶端通過類似 AppRuntims.Instance.GetService<IOrderService>().SaveOrder(orderInfo) 形式訪問遠程服務創建訂單.
    4. 數據以json格式傳輸.
  4. 解決方案及實現

    1. 為了便於處理,我們定義了一個空介面IApiService,用來標識服務介面.

    2. 遠程服務客戶端代理

      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);
          }
      }
    3. 遠程服務端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;
      }
    4. 本地服務中心通過介面名和方法名,找出具體的實現類的方法,並使用傳遞的參數執行,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;
    5. 服務配置,指示具體的服務的遠程地址,當未配置的服務預設為本地服務.

      [
        {
          "ServiceId": "XZL.Api.IOrderService",
          "Address": "http://localhost:8801/api/svc"
        },
        {
          "ServiceId": "XZL.Api.IUserService",
          "Address": "http://localhost:8802/api/svc"
        } 
      ]
    6. 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;
      }
    7. 為了性能影響,我們在程式啟動的時候可以將當前所有的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;
      }
    8. 具體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);
              }
      }
  5. 結語

    經過以上改造後, 我們便可很方便的通過形如 AppRuntime.Instance.GetService<TService>().MethodXX()無感的訪問遠程服務, 服務是部署在遠程還是在本地以dll依賴形式存在,這個便對調用者透明瞭.無縫的對接上了大家固有習慣.
    PS: 但是此番改造後, 遺留下來了另外一個問題: 客戶端調用遠程服務,需要手動創建一個服務代理( 從 RemoteServiceProxy 繼承),雖然每個代理很方便寫,只是文中提到的簡單兩句話,但終究顯得繁瑣, 是否有一種方式能夠根據遠程api介面動態的生成這個客戶端代理呢? 答案是肯定的,因本文較長了,留在下篇再續.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 概念:立即執行函數顧名思義就是函數定義好之後立即執行。函數表達式方式:函數表達式後面加括弧()即可立即執行函數。 var xmlhttpUtil = function () { function getText(url,callback){ alert("dog"); }(); 匿名函數方式:匿名函... ...
  • 1. 瀏覽器載入 (1) 同步載入 在網頁中,瀏覽器載入js文件的方式是通過<script>標簽。如下所示: <script>標簽很方便,只要加入後,瀏覽器便可讀取並運行,但是在讀取的時候,瀏覽器是按照<script>標簽的出現順序,讀取Javascript文件,然後立即運行,導致在多個文件相互依賴 ...
  • 1.評估功能變數名稱類型 .com——商業實體 .edu——僅限有學位或更高等學歷授予資格的高等教育使用 .gov——僅限政府使用 .net——與Internrt網路支持相關的團體,通常是Internet服務提供商或者電信公司 .org——非盈利組織 2.HTML -HTML是一套標記符號或者代碼集,他們插 ...
  • 一、理解 1、什麼是模塊、模塊化? 將一個複雜的程式依據一定的規範封裝成幾個文件,併進行組合在一起 2、為什麼要模塊化? 降低複雜度,提高解耦性,部署方便 3、模塊化的好處 避免命名衝突(減少命名空間污染) 更好的分離,按需載入 更高復用性 高可維護性 4、頁面引入載入script 二、模塊化規範 ...
  • 1.Socket簡介 Socket也稱作“套接字“,是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操作抽象為幾個簡單的介面供應用層調用以實現進程在網路中通信。它分為流式套接字和數據包套接字,分別對應網路傳輸控制層的TCP和UDP協議。TCP協議是一種面向連接的、可靠的、基於位元組流的傳輸 ...
  • 相信看過 每天學點SpringCloud(一):簡單服務提供者消費者調用的同學都發現了,在最後消費者調用提供者的時候把提供者的地址硬編碼在了代碼中,這樣的方式肯定是不行的,今天,我們就是要Eureka來解決這個問題 創建Eureka服務 1.我們在原先項目的基礎上再新建一個項目cloud-demo- ...
  • 微服務 將整體功能按著模塊劃分成多個獨立的單元,這些單元可以獨立部署,它們之前通過輕量級的web api方式進行通訊,對於微服務框架來說,最流行的就是springcloud和Service Fabric,前者是java開發,後者是.net的產品,今天主要介紹一下springcloud! 參考文章:h ...
  • 系統介紹: 1.系統採用主流的 SSM 框架 jsp JSTL bootstrap html5 (PC瀏覽器使用) 2.springmvc +spring4.3.7+ mybaits3.3 SSM 普通java web(非maven, 附贈pom.xml文件) 資料庫:mysql 3.開發工具:my ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...