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

原文:基於.net core 微服務的另類實現html

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

  1. 背景

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


    image

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

  2. 問題提出


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

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

    1. 爲了便於處理,咱們定義了一個空接口IApiService,用來標識服務接口.c#

    2. 遠程服務客戶端代理api

      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. 服務配置,指示具體的服務的遠程地址,當未配置的服務默認爲本地服務.

      app

      [
        {
          "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接口動態的生成這個客戶端代理呢? 答案是確定的,因本文較長了,留在下篇再續附上動態編譯文章連接. https://www.cnblogs.com/xie-zhonglai/p/dynamic_compilation_netstandard.html.

相關文章
相關標籤/搜索