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


系列目錄

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個小伙伴了。


免責聲明!

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



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