net core天馬行空系列:移植Feign,結合Polly,實現回退,熔斷,重試,超時,做最好用的聲明式http服務調用端

  • 2020 年 3 月 18 日
  • 筆記

系列目錄

1.net core天馬行空系列:原生DI+AOP實現spring boot註解式編程

2.net core天馬行空系列: 泛型倉儲和聲明式事物實現最優雅的crud操作

3.net core天馬行空系列: 一個介面多個實現類,利用mixin技術通過自定義服務名,實現精準屬性注入

4.net core天馬行空系列:SummerBoot,將SpringBoot的先進理念與C#的簡潔優雅合二為一

正文開始

   hi,大家好,我就是高產似母豬的三合。距離上一篇文章,好像有點久了,我也不知道我都在幹嘛,啊哈哈哈哈,今天這篇文章,從幾個問題開始。

Feign是什麼?

      Feign它是一個聲明式http服務調用端,做一個類比,如果把httpClient比作ado.net,那麼feign就相當於ef/dapper。他是更頂層的封裝,提供了更簡單更友好的調用方式,更適合面向介面的編程習慣,通過介面+註解,就能實現http訪問。因為微軟爸爸認為網路請求是IO操作,用非同步更為合適,所以feign只支援非同步返回,即task<int>這種形式。

Feign用法

首先,註冊服務的時候添加以下紅線框出來的服務。

 

然後定義介面如下

最後注入使用

 

 

已經有webApiClient、refit了,為什麼還要移植feign呢?

因為我覺得這兩個組件還有一些不夠完美的地方,比如沒有和微軟的DI更緊密的結合在一起,不是完全面向介面編程導致擴展性不佳, 不支援回退,熔斷,重試,超時等,於是我移植了java的feign項目,就有了這篇文章。

什麼是回退、熔斷、重試、超時?

回退:通俗來說就是備胎,如果方法調用報錯了,比如調用的服務掛了,調用超時了等,就返回備胎裡面的內容。定義回退類QueryEmployeeFallBack,該類也要實現IQueryEmployee介面

namespace Example.Feign  {      public class QueryEmployeeFallBack : IQueryEmployee      {          public async Task<Employee> FindAsync([Param("")] string id, [Body] Employee user)          {              await Task.Delay(1000);              return new Employee() {Name = "fallBack"};          }            public async Task<List<Employee>> GetEmployeeAsync(string url, int ab)          {              return await Task.FromResult(new List<Employee>(){new Employee(){Name = "fallBack"}});          }            public Task<int> GetEmployeeCountAsync()          {              return Task.FromResult(0);          }      }  }

重試:字面意思,就是方法調用報錯了,則進行重試

超時:即給方法的執行限定時間,如果超出了這個時間,就默認為方法執行失敗。

斷路:如果執行方法出現錯誤,執行多次還是錯誤的話,就認為這個方法可能掛了,在設定的時間內就不會去調用這個方法,而是直接返回錯誤,實際場景比如調用的api服務可能因為訪問量太大,快掛了,這時候我們試了3次還是報錯後,在10s內就不會真正去訪問這個api介面,而是直接報錯,就可以避免給api介面造成更多壓力。

移植的feign屬於summerBoot項目的一部分,基於MIT協議開源,歡迎star,感興趣的可以加Q群799648362

github地址:https://github.com/TripleView/SummerBoot

nuget搜索:SummerBoot

效果圖

定義一個employee類用來測試

先新建一個webApi項目,寫幾個介面供http訪問。

 

 測試1.get請求,返回int類型,符合預期

 

 測試2,帶參數的get請求,返回List<Employee>,api介面正常接收到請求參數,調用返回正常,符合預期。

 

 

 

 測試3.發送post請求,在body里添加內容,同時利用param註解改變參數名稱,發送接收都正常,符合預期。

 

 

 

 

 

 

 測試4,同時訪問3個api介面,返回正常,符合預期

 

 測試5,超時+回退,在feign客戶端里定義超時時間為2000毫秒,即2s,api介面那邊添加一行讓執行緒停3s的程式碼,如果沒超時應該返回1,但是超時了,日誌里也顯示超時,這時候就會進入回退,返回QueryEmployeeFallBack類對應方法的值0。

 

 

 

 

 

 

 

 

 

 

 

 測試6,超時+重試+回退。定義重試3次,每次間隔1s,其餘和測試5一致,日誌顯示,重試3次後進入回退,返回0,符合預期。

 

 

 

 

 測試7,超時+重試+回退+熔斷。關閉api介面,模擬服務掛了,定義重試3次,每次間隔1s,開啟斷路。日誌提示重試3次,之間有進入熔斷,返回0,符合預期。

 

 

  

 

 

源碼解析,授人以漁

1.通過IL程式碼動態生成介面的實現類

核心類FeignProxyBuilder,這裡要特別感謝蘇州的黑洞視界同學,在IL程式碼方面給了我很大的幫助。這個類的核心思想,就是動態生成介面的實現類,生成的實現類只是一個空殼,他裡面只有3個欄位,第一個,List<object> 類型,用來存放調用方法時的實際參數,第二個,IServiceProvider,由構造函數傳入,第三個,httpService類,這個類就是真正執行網路請求的類,也是由構造函數傳入,這個實現類的作用就是當調用介面里的方法時,利用IL程式碼把傳遞進來的參數收集起來存放到list<object>的欄位里,然後調用httpSerivce的方法,把參數集合,方法體資訊methodInfo,還有IServiceProvider一起傳入到httpService的代理方法里,httpService處理完畢後獲得結果,利用IL程式碼清除掉list<object>欄位里存放的參數,保證每次調用方法時,list<object>里都是空的,避免上次調用方法的參數沒清空,影響到本次調用,最後返回方法調用結果。具體如何動態生成這個實現類,程式碼里都有詳細註解了,同時IL處理時有個要義,調用實例前,一定都要載入類本身,即Ldarg_0。

public interface IProxyBuilder      {          Object Build(Type interfaceType,params object[] constructor);          T Build<T>(params object[] constructor);  }

namespace SummerBoot.Feign  {      public interface IFeignProxyBuilder:IProxyBuilder      {      }  }

public class FeignProxyBuilder : IFeignProxyBuilder      {          public static Dictionary<string, MethodInfo> MethodsCache { get; set; } = new Dictionary<string, MethodInfo>();            private Type targetType;            private static ConcurrentDictionary<string, Type> TargetTypeCache { set; get; } =              new ConcurrentDictionary<string, Type>();              public object Build(Type interfaceType, params object[] constructor)          {              var cacheKey = interfaceType.FullName;              var resultType = TargetTypeCache.GetOrAdd(cacheKey, (s)=> BuildTargetType(interfaceType, constructor));              var result = Activator.CreateInstance(resultType, args: constructor);              return result;          }            /// <summary>          /// 動態生成介面的實現類          /// </summary>          /// <param name="interfaceType"></param>          /// <param name="constructor"></param>          /// <returns></returns>          private Type BuildTargetType(Type interfaceType, params object[] constructor)          {              targetType = interfaceType;              string assemblyName = targetType.Name + "ProxyAssembly";              string moduleName = targetType.Name + "ProxyModule";              string typeName = targetType.Name + "Proxy";                AssemblyName assyName = new AssemblyName(assemblyName);              AssemblyBuilder assyBuilder = AssemblyBuilder.DefineDynamicAssembly(assyName, AssemblyBuilderAccess.Run);              ModuleBuilder modBuilder = assyBuilder.DefineDynamicModule(moduleName);                //新類型的屬性              TypeAttributes newTypeAttribute = TypeAttributes.Class | TypeAttributes.Public;              //父類型              Type parentType;              //要實現的介面              Type[] interfaceTypes;                if (targetType.IsInterface)              {                  parentType = typeof(object);                  interfaceTypes = new Type[] { targetType };              }              else              {                  parentType = targetType;                  interfaceTypes = Type.EmptyTypes;              }              //得到類型生成器                          TypeBuilder typeBuilder = modBuilder.DefineType(typeName, newTypeAttribute, parentType, interfaceTypes);                //定義一個欄位存放httpService              var httpType = typeof(HttpService);              FieldBuilder httpServiceField = typeBuilder.DefineField("httpService",                  httpType, FieldAttributes.Public);                //定義一個欄位存放IServiceProvider              var iServiceProviderType = typeof(IServiceProvider);              FieldBuilder serviceProviderField = typeBuilder.DefineField("iServiceProvider",                  iServiceProviderType, FieldAttributes.Public);                //定義一個集合存放參數集合              FieldBuilder paramterArrField = typeBuilder.DefineField("paramterArr",                  typeof(List<object>), FieldAttributes.Public);              //創建構造函數              ConstructorBuilder constructorBuilder =                  typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { httpType, iServiceProviderType });                //il創建構造函數,對httpService和IServiceProvider兩個欄位進行賦值,同時初始化存放參數的集合              ILGenerator ilgCtor = constructorBuilder.GetILGenerator();              ilgCtor.Emit(OpCodes.Ldarg_0); //載入當前類              ilgCtor.Emit(OpCodes.Ldarg_1);              ilgCtor.Emit(OpCodes.Stfld, httpServiceField);                ilgCtor.Emit(OpCodes.Ldarg_0); //載入當前類              ilgCtor.Emit(OpCodes.Ldarg_2);              ilgCtor.Emit(OpCodes.Stfld, serviceProviderField);                ilgCtor.Emit(OpCodes.Ldarg_0); //載入當前類              ilgCtor.Emit(OpCodes.Newobj, typeof(List<object>).GetConstructors().First());              ilgCtor.Emit(OpCodes.Stfld, paramterArrField);              ilgCtor.Emit(OpCodes.Ret); //返回                MethodInfo[] targetMethods = targetType.GetMethods();                foreach (MethodInfo targetMethod in targetMethods)              {                  //只挑出virtual的方法                  if (targetMethod.IsVirtual)                  {                      //快取介面的方法體,便於後續將方法體傳遞給httpService                      string methodKey = Guid.NewGuid().ToString();                      MethodsCache[methodKey] = targetMethod;                        //得到方法的各個參數的類型和參數                      var paramInfo = targetMethod.GetParameters();                      var parameterType = paramInfo.Select(it => it.ParameterType).ToArray();                      var returnType = targetMethod.ReturnType;                      //方法返回值只能是task,即只支援非同步,因為http操作是io操作                      if (!typeof(Task).IsAssignableFrom(returnType)) throw new Exception("return type must be task<>");                      var underType = returnType.IsGenericType ? returnType.GetGenericArguments().First() : returnType;                        //通過emit生成方法體                      MethodBuilder methodBuilder = typeBuilder.DefineMethod(targetMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual, targetMethod.ReturnType, parameterType);                      ILGenerator ilGen = methodBuilder.GetILGenerator();                        MethodInfo executeMethod = null;                      var methodTmp = httpType.GetMethod("ExecuteAsync");                        if (methodTmp == null) throw new Exception("找不到執行方法");                      executeMethod = methodTmp.IsGenericMethod ? methodTmp.MakeGenericMethod(underType) : methodTmp;                        // 棧底放這玩意,載入欄位前要載入類實例,即Ldarg_0                      ilGen.Emit(OpCodes.Ldarg_0);                      ilGen.Emit(OpCodes.Ldfld, httpServiceField);                        //把所有參數都放到list<object>里                      ilGen.Emit(OpCodes.Ldarg_0);                      ilGen.Emit(OpCodes.Ldfld, paramterArrField);                      for (int i = 0; i < parameterType.Length; i++)                      {                          ilGen.Emit(OpCodes.Dup);                          ilGen.Emit(OpCodes.Ldarg_S, i + 1);                          if (parameterType[i].IsValueType)                          {                              ilGen.Emit(OpCodes.Box, parameterType[i]);                          }                          ilGen.Emit(OpCodes.Callvirt, typeof(List<object>).GetMethod("Add"));                      }                        // 當前棧[httpServiceField paramterArrField]                      //從快取里取出方法體                      ilGen.Emit(OpCodes.Call,                          typeof(FeignProxyBuilder).GetMethod("get_MethodsCache", BindingFlags.Static | BindingFlags.Public));                      ilGen.Emit(OpCodes.Ldstr, methodKey);                      ilGen.Emit(OpCodes.Call, typeof(Dictionary<string, MethodInfo>).GetMethod("get_Item"));                        ilGen.Emit(OpCodes.Ldarg_0);                      ilGen.Emit(OpCodes.Ldfld, serviceProviderField);                        ilGen.Emit(OpCodes.Callvirt, executeMethod);                      //清空list里的參數                      ilGen.Emit(OpCodes.Ldarg_0);                      ilGen.Emit(OpCodes.Ldfld, paramterArrField);                      ilGen.Emit(OpCodes.Callvirt, typeof(List<object>).GetMethod("Clear"));                      // pop the stack if return void                      if (targetMethod.ReturnType == typeof(void))                      {                          ilGen.Emit(OpCodes.Pop);                      }                        // complete                      ilGen.Emit(OpCodes.Ret);                      typeBuilder.DefineMethodOverride(methodBuilder, targetMethod);                  }              }                var resultType = typeBuilder.CreateTypeInfo().AsType();              return resultType;          }            public T Build<T>(params object[] constructor)          {              return (T)this.Build(typeof(T), constructor);          }      }

 

2.完全面向介面設計的feign源碼

feign中的介面

1.IClient介面,這個介面代表的就是實際執行網路請求的類,他的默認實現類DefaultFeignClient 基於httpClient,也可以自己實現這個介面,換成其他網路請求客戶端。在feign框架調用iclient之前,會把所有請求資訊封裝到requestTemplate里,iclient執行完畢後,也需要把所有返回資訊封裝到responseTemplate里,利用這兩個類,解耦了feign框架與具體iclient實現類。

namespace SummerBoot.Feign  {      /// <summary>      /// 實際執行http請求的客戶端      /// </summary>      public interface IClient      {          Task<ResponseTemplate> ExecuteAsync(RequestTemplate requestTemplate, CancellationToken cancellationToken);          /// <summary>          /// 默認的IClient類,內部採用httpClient          /// </summary>          public class DefaultFeignClient : IClient          {              private IHttpClientFactory HttpClientFactory { get; }              public DefaultFeignClient(IHttpClientFactory iHttpClientFactory)              {                  HttpClientFactory = iHttpClientFactory;              }                public async Task<ResponseTemplate> ExecuteAsync(RequestTemplate requestTemplate, CancellationToken cancellationToken)              {                  var httpClient = HttpClientFactory.CreateClient();                    var httpRequest = new HttpRequestMessage(requestTemplate.HttpMethod, requestTemplate.Url);                    if (requestTemplate.HttpMethod == HttpMethod.Post)                  {                      httpRequest.Content = new StringContent(requestTemplate.Body);                  }                    //處理header                  foreach (var requestTemplateHeader in requestTemplate.Headers)                  {                      var uppperKey = requestTemplateHeader.Key.ToUpper();                        var key = uppperKey.Replace("-", "");                      //判斷普通標頭                      if (HttpHeaderSupport.RequestHeaders.Contains(key))                      {                          httpRequest.Headers.Remove(requestTemplateHeader.Key);                          httpRequest.Headers.Add(requestTemplateHeader.Key, requestTemplateHeader.Value);                      }                      //判斷body標頭                      else if (HttpHeaderSupport.ContentHeaders.Contains(key))                      {                          httpRequest.Content.Headers.Remove(requestTemplateHeader.Key);                          httpRequest.Content.Headers.Add(requestTemplateHeader.Key, requestTemplateHeader.Value);                      }                      //自定義標頭                      else                      {                          httpRequest.Headers.TryAddWithoutValidation(requestTemplateHeader.Key,                              requestTemplateHeader.Value);                      }                  }                    var httpResponse = await httpClient.SendAsync(httpRequest, cancellationToken);                    //把httpResponseMessage轉化為responseTemplate                  var result = await ConvertResponseAsync(httpResponse);                    return result;              }                /// <summary>              /// 把httpResponseMessage轉化為responseTemplate              /// </summary>              /// <param name="responseMessage"></param>              /// <returns></returns>              private async Task<ResponseTemplate> ConvertResponseAsync(HttpResponseMessage responseMessage)              {                  var responseTemplate = new ResponseTemplate                  {                      HttpStatusCode = responseMessage.StatusCode                  };                    var headers = responseMessage.Headers;                  foreach (var httpResponseHeader in headers)                  {                      responseTemplate.Headers.Add(httpResponseHeader);                  }                    var stream = new MemoryStream();                  await responseMessage.Content.CopyToAsync(stream);                  stream.Seek(0, SeekOrigin.Begin);                  responseTemplate.Body = stream;                  return responseTemplate;              }          }      }  }

2.IFeignEncoder介面,這個介面的作用是定義序列化器,比如在post請求中,定義如何序列化參數然後放到body里,默認實現類DefaultEncoder 基於json,也可以實現該介面,換成其他種序列化方式。

namespace SummerBoot.Feign  {      /// <summary>      /// 序列化介面      /// </summary>      public interface IFeignEncoder      {          void Encoder(object obj, RequestTemplate requestTemplate);          /// <summary>          /// 默認的序列化器          /// </summary>          public class DefaultEncoder : IFeignEncoder          {              public void Encoder(object obj, RequestTemplate requestTemplate)              {                  var objStr = JsonConvert.SerializeObject(obj);                  requestTemplate.Body = objStr;              }          }      }  }

3.IFeignDecoder介面,這個介面的作用是定義反序列化器,作用是將網路請求成功後收到的資訊反序列化成結果,默認實現類DefaultDecoder基於json,也可以實現該介面,換成其他種反序列化方式

namespace SummerBoot.Feign  {      /// <summary>      /// 反序列化介面      /// </summary>      public interface IFeignDecoder      {          object Decoder(ResponseTemplate responseTemplate, Type type);            T Decoder<T>(ResponseTemplate responseTemplate);            /// <summary>          /// 默認的反序列化器          /// </summary>          public class DefaultDecoder : IFeignDecoder          {              public object Decoder(ResponseTemplate responseTemplate, Type type)              {                  if (responseTemplate.HttpStatusCode == HttpStatusCode.NotFound ||                      responseTemplate.HttpStatusCode == HttpStatusCode.NoContent)                  {                      return type.GetDefaultValue();                  }                    if (responseTemplate.Body == null) return null;                  var body = responseTemplate.Body;                  var str = body.ConvertToString();                  return JsonConvert.DeserializeObject(str, type);              }                public T Decoder<T>(ResponseTemplate responseTemplate)              {                  return (T)this.Decoder(responseTemplate, typeof(T));              }          }      }  }

IRequestInterceptor介面,這個介面的作用是在網路請求前,攔截該請求,可以做一些自定義操作,比如添加授權,該介面沒有默認實現類,如果有攔截需求可以實現該介面並註冊到DI容器即可攔截。

namespace SummerBoot.Feign  {      public interface IRequestInterceptor      {          void Apply(RequestTemplate requestTemplate);      }  }

feign在添加服務的時候都是採用嘗試註冊,所以如果有自定義的介面,在添加feign服務之前註冊到DI容器里,即可替換feign的默認實現。

 

類似這樣

 

 

 feign中的註解

1.FeignClientAttribute,這個註解只能用在介面上,添加了這個註解的介面都會被當成feign客戶端註冊到DI容器里,主要有服務名稱,url地址,回退類,路徑等參數。

namespace SummerBoot.Feign  {      [AttributeUsage(AttributeTargets.Interface)]      public class FeignClientAttribute:Attribute      {          /// <summary>          /// 服務名稱          /// </summary>          public string Name { get; }          /// <summary>          /// url地址          /// </summary>          public string Url { get; }          /// <summary>          /// 回退類          /// </summary>          public Type FallBack { get; }          public Type Configuration { get; }          public bool Decode404 { get; }          public string Qualifier { get; }          /// <summary>          /// 路徑          /// </summary>          public string Path { get; }          /// <summary>          ///          /// </summary>          /// <param name="name">服務名稱</param>          /// <param name="url">url地址</param>          /// <param name="fallBack">回退類</param>          /// <param name="configuration"></param>          /// <param name="decode404"></param>          /// <param name="qualifier"></param>          /// <param name="path">路徑</param>          public FeignClientAttribute(string name,string url="",Type fallBack=null,Type configuration=null,bool decode404=false,string qualifier="",string path="")          {              Name = name;              Url = url;              FallBack = fallBack;              Configuration = configuration;              Decode404 = decode404;              Qualifier = qualifier;              Path = path;          }      }  }

2.HeadersAttribute註解,用來添加請求頭,params類型,可以添加N個請求頭

namespace SummerBoot.Feign  {      [AttributeUsage(AttributeTargets.Method)]      public class HeadersAttribute:Attribute      {          public string[] Param { get; }          public HeadersAttribute(params  string[] param)          {              Param = param;          }      }  }

3.GetMappingAttribute和PostMappingAttribute,get請求和post請求的註解,只有一個value的參數,代表路徑。

 public class PostMappingAttribute : HttpMappingAttribute      {          public PostMappingAttribute(string value):base(value)          {          }      }

namespace SummerBoot.Feign  {      public class GetMappingAttribute:HttpMappingAttribute      {          public GetMappingAttribute(string value):base(value)          {            }      }  }

4.BodyAttribute,在post請求中,給class類型的參數添加,即代表將該參數添加到請求的body里。

namespace SummerBoot.Feign  {      [AttributeUsage(AttributeTargets.Parameter)]      public class BodyAttribute:Attribute      {        }  }

5.ParamAttribute,參數註解,主要是用來實現重命名參數的效果,比如介面方法中這個參數名叫employee,但是url中是user,那麼利用這個註解即可重命名為user,[param(“user”)]即可。

namespace SummerBoot.Feign  {      [AttributeUsage(AttributeTargets.Parameter)]      public class ParamAttribute : Attribute      {          public string Value { get; }            public ParamAttribute(string value = "")          {              Value = value;          }      }  }

6.PollyAttribute註解,主要定義了重試,超時,斷路的一些策略。

namespace SummerBoot.Core  {      [AttributeUsage(AttributeTargets.Interface)]      public class PollyAttribute:Attribute      {          public int Retry { get; }          public int RetryInterval { get; }          public bool OpenCircuitBreaker { get; }          public int ExceptionsAllowedBeforeBreaking { get; }            public int DurationOfBreak { get; }          public int Timeout { get; }          /// <summary>          ///          /// </summary>          /// <param name="retry">重試次數</param>          /// <param name="retryInterval">重試間隔,單位毫秒</param>          /// <param name="openCircuitBreaker">是否開啟斷路</param>          /// <param name="exceptionsAllowedBeforeBreaking">錯誤幾次進入斷路</param>          /// <param name="durationOfBreak">斷路時間</param>          /// <param name="timeout">超時時間,單位毫秒</param>          public PollyAttribute(int retry=0,int retryInterval=500,bool openCircuitBreaker=false,int exceptionsAllowedBeforeBreaking = 3, int durationOfBreak = 1000, int timeout=0)          {              Retry = retry;              RetryInterval = retryInterval;              OpenCircuitBreaker = openCircuitBreaker;              ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking;              DurationOfBreak = durationOfBreak;              Timeout = timeout;          }      }  }

核心類FeignAspectSupport,這個類就是一個膠水類,負責解析註解,獲得各種介面實現類,然後進行方法調用,獲取最後結果,主要地方都有注釋。

namespace SummerBoot.Feign  {      public class FeignAspectSupport      {          private IServiceProvider _serviceProvider;            public async Task<T> BaseExecuteAsync<T>(MethodInfo method, object[] args, IServiceProvider serviceProvider)          {              _serviceProvider = serviceProvider;              //獲得具體的client客戶端              var feignClient = serviceProvider.GetService<IClient>();              //序列化器與反序列化器              var encoder = serviceProvider.GetService<IFeignEncoder>();              var decoder = serviceProvider.GetService<IFeignDecoder>();                //讀取feignClientAttribute里的資訊;              //介面類型              var interfaceType = method.DeclaringType;              if (interfaceType == null) throw new Exception(nameof(interfaceType));              var feignClientAttribute = interfaceType.GetCustomAttribute<FeignClientAttribute>();              var url = feignClientAttribute.Url;              var path = feignClientAttribute.Path;              var path2 = string.Empty;              var clientName = feignClientAttribute.Name;              var requestPath = url + path;              var requestTemplate = new RequestTemplate();                //獲得請求攔截器              var requestInterceptor = serviceProvider.GetService<IRequestInterceptor>();                //處理請求頭邏輯              ProcessHeaders(method, requestTemplate);                //處理get邏輯              var getMappingAttribute = method.GetCustomAttribute<GetMappingAttribute>();              if (getMappingAttribute != null)              {                  path2 = getMappingAttribute.Value;                  requestTemplate.HttpMethod = HttpMethod.Get;              }                //處理post邏輯              var postMappingAttribute = method.GetCustomAttribute<PostMappingAttribute>();              if (postMappingAttribute != null)              {                  path2 = postMappingAttribute.Value;                  requestTemplate.HttpMethod = HttpMethod.Post;              }                var urlTemp = (requestPath + path2).ToLower();                requestTemplate.Url = GetUrl(urlTemp);                //處理參數,因為有些參數需要拼接到url里,所以要在url處理完畢後才能處理參數              ProcessParameter(method, args, requestTemplate, encoder);                //如果存在攔截器,則進行攔截              if(requestInterceptor!=null) requestInterceptor.Apply(requestTemplate);                var responseTemplate = await feignClient.ExecuteAsync(requestTemplate, new CancellationToken());                //判斷方法返回值是否為非同步類型              var isAsyncReturnType = method.ReturnType.IsAsyncType();              //返回類型              var returnType = isAsyncReturnType ? method.ReturnType.GenericTypeArguments.First() : method.ReturnType;                var resultTmp = (T)decoder.Decoder(responseTemplate, returnType);                return resultTmp;          }            private string GetUrl(string urlTemp)          {              Func<string,string> func = (string s) =>              {                  s = s.Replace("//", "/");                  s = s.Replace("///", "/");                  s = "http://" + s;                  return s;              };                if (urlTemp.Length < 8)              {                  return func(urlTemp);              }                var isHttp = urlTemp.Substring(0, 7) == "http://";              var isHttps = urlTemp.Substring(0, 8) == "https://";              if (!isHttp && !isHttps)              {                  return func(urlTemp);              }                if (isHttp)              {                  urlTemp = urlTemp.Substring(7, urlTemp.Length - 7);                  return func(urlTemp);              }                if (isHttps)              {                  urlTemp=urlTemp.Substring(8, urlTemp.Length - 8);                  urlTemp = urlTemp.Replace("//", "/");                  urlTemp = urlTemp.Replace("///", "/");                  urlTemp = "https://" + urlTemp;              }                return urlTemp;          }            /// <summary>          /// 處理請求頭邏輯          /// </summary>          /// <param name="method"></param>          /// <param name="requestTemplate"></param>          private void ProcessHeaders(MethodInfo method, RequestTemplate requestTemplate)          {              var headersAttribute = method.GetCustomAttribute<HeadersAttribute>();              if (headersAttribute != null)              {                  var headerParams = headersAttribute.Param;                  foreach (var headerParam in headerParams)                  {                      if (headerParam.HasIndexOf(':'))                      {                          var headerParamArr = headerParam.Split(":");                          var key = headerParamArr[0].Trim();                          var keyValue = headerParamArr[1];                          var hasHeaderKey = requestTemplate.Headers.TryGetValue(key, out var keyList);                          if (!hasHeaderKey) keyList = new List<string>();                          keyList.Add(keyValue.Trim());                          if (!hasHeaderKey) requestTemplate.Headers.Add(key, keyList);                      }                  }              }          }            /// <summary>          /// 處理參數          /// </summary>          private void ProcessParameter(MethodInfo method, object[] args, RequestTemplate requestTemplate, IFeignEncoder encoder)          {              var parameterInfos = method.GetParameters();              //所有參數里,只能有一個body的註解              var hasBodyAttribute = false;              //參數集合              var parameters = new Dictionary<string, string>();              //url              var url = requestTemplate.Url;                for (var i = 0; i < parameterInfos.Length; i++)              {                  var arg = args[i];                  var parameterInfo = parameterInfos[i];                  var parameterType = parameterInfos[i].ParameterType;                  var paramAttribute = parameterInfo.GetCustomAttribute<ParamAttribute>();                  var bodyAttribute = parameterInfo.GetCustomAttribute<BodyAttribute>();                  var parameterName = parameterInfos[i].Name;                    if (paramAttribute != null && bodyAttribute != null)                  {                      throw new Exception(parameterType.Name + "can not accept parameterAttrite and bodyAttribute");                  }                    if (hasBodyAttribute) throw new Exception("bodyAttribute just only one");                  if (bodyAttribute != null) hasBodyAttribute = true;                    var parameterTypeIsString = parameterType.IsString();                    //處理param類型                  if ((parameterTypeIsString || parameterType.IsValueType) && bodyAttribute == null)                  {                      parameterName = paramAttribute != null? paramAttribute.Value.GetValueOrDefault(parameterName):parameterName;                      parameters.Add(parameterName, arg.ToString());                  }                    //處理body類型                  if (!parameterTypeIsString && parameterType.IsClass && bodyAttribute != null)                  {                      encoder.Encoder(args[i], requestTemplate);                  }              }                var strParam = string.Join("&", parameters.Select(o => o.Key + "=" + o.Value));              if (strParam.HasText()) url = string.Concat(url, '?', strParam);              requestTemplate.Url = url;          }      }  }

View Code

核心類httpService,這個類是FeignAspectSupport的子類,主要是負責解析polly的各種策略,然後組合成polly的policy,最後調用FeignAspectSupport的方法, 實現重試,斷路,超時等

namespace SummerBoot.Feign  {      public class HttpService : FeignAspectSupport      {            public async Task<T> ExecuteAsync<T>(List<object> originArgs, MethodInfo method, IServiceProvider serviceProvider)          {              var args = new List<object>();              originArgs.ForEach(it => args.Add(it));                var interfaceType = method.DeclaringType;              if (interfaceType == null) throw new Exception(nameof(interfaceType));              var feignClientAttribute = interfaceType.GetCustomAttribute<FeignClientAttribute>();              var name = feignClientAttribute.Name;              var fallBack = feignClientAttribute.FallBack;              object interfaceTarget;              //處理熔斷重試超時邏輯              IAsyncPolicy<T> policy = Policy.NoOpAsync<T>();              var logFactory = serviceProvider.GetService<ILoggerFactory>();              var log = logFactory.CreateLogger<HttpService>();                if (fallBack != null)              {                  policy = policy.WrapAsync(Policy<T>.Handle<Exception>()                        .FallbackAsync<T>(async (x) =>                        {                            interfaceTarget = serviceProvider.GetServiceByName(interfaceType.Name + "FallBack", interfaceType);                            var fallBackMethod = interfaceTarget.GetType().GetMethods().First(it => it.ReturnType == method.ReturnType && it.Name == method.Name);                            var fallBackTask = fallBackMethod.Invoke(interfaceTarget, args.ToArray()) as Task<T>;                            if (fallBackTask == null) throw new Exception("fallBack method ReturnValue error");                            return await fallBackTask;                        }));              }                var pollyAttribute = interfaceType.GetCustomAttribute<PollyAttribute>();                if (pollyAttribute != null)              {                  if (pollyAttribute.Retry > 0)                  {                      policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(pollyAttribute.Retry, i => TimeSpan.FromMilliseconds(pollyAttribute.RetryInterval), (                          (exception, retryCount, context) =>                          {                              log.LogError($"feign客戶端{name}:開始第 " + retryCount + "次重試,當前時間:" + DateTime.Now);                          })));                  }                    if (pollyAttribute.Timeout > 0)                  {                      policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(pollyAttribute.Timeout), Polly.Timeout.TimeoutStrategy.Pessimistic, (                          (context, span, arg3, arg4) =>                          {                              log.LogError($"feign客戶端{name}:超時提醒,當前時間:" + DateTime.Now);                              return Task.CompletedTask;                          })));                  }                    if (pollyAttribute.OpenCircuitBreaker)                  {                      policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(pollyAttribute.ExceptionsAllowedBeforeBreaking, TimeSpan.FromMilliseconds(pollyAttribute.DurationOfBreak), (                      //policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1500), (                          (exception, span, arg3) =>                          {                              log.LogError($"feign客戶端{name}:熔斷: {span.TotalMilliseconds } ms, 異常: " + exception.Message + DateTime.Now);                          }), (context =>                      {                          log.LogError("feign客戶端{name}:熔斷器關閉了" + DateTime.Now);                      }),                          (() => { log.LogError("feign客戶端{name}:熔斷時間到,進入半開狀態" + DateTime.Now); })));                  }                    return await policy.ExecuteAsync(async () => await base.BaseExecuteAsync<T>(method, args.ToArray(), serviceProvider));              }                return await base.BaseExecuteAsync<T>(method, args.ToArray(), serviceProvider);              }      }  }

View Code

 

寫在最後

     他山之石,可以攻玉。如果覺得這篇文章不錯,不妨點個贊咯。如果有好的想法,不管新人,還是大神,都歡迎貢獻程式碼,sb項目已經有3個小夥伴了。