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


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM