Refit 集成consul在asp.net core中的实践

refit consul

Refit  WebApiClient  Feign等都是支持声名式的Restful服务调用的开源组件。

这个几个组件都综合研究总结了下,Refit fork数多,使用文档易懂,提供的功能基本都满足我的要求。

同时Refit本身集成了HttpClientFactory(Refit.HttpClientFactory)。

综上最后还是选择了Refit。

然而我的项目是使用Consul作为服务注册中心。

Refit、WebApiClient、Feign 这个几个.Net core 社区比较流行的http客户端Restful资源请求组件都没有集成Consul服务发现功能。

Steeltoe扩展了Refit的Euerka的服务发现,配合Refit.HttpClientFactory可以很好的声明服务调用。

在google搜索了下Refit consul关键字,搜索出来的基本都是介绍Refit与Consul的基础使用的文章。

看来只有靠自己造个轮子了。

研究了下Steeltoe组件Refit的Euerka的服务发现。

要集成Consul需要实现一个ConsulHttpMessageHandler,看了下Steeltoel的DiscoveryHttpMessageHandler类代码,关联的文件太多,借鉴它的写法太麻烦了。

原本想放弃了,接着研究了下Refit的相关代码与httpclientfactory相关文章,豁然开朗。

原来很容易实现,只是自己之前没有看懂而已。

只需写一个类继承DelegatingHandler类,覆写SendAsync方法,并把该类注册进去替换缺省的HttpMessageHandler。

核心代码

namespace RefitConsul
{
    public class ConsulDiscoveryDelegatingHandler : DelegatingHandler
    {
        private readonly ConsulClient _consulClient;
        private readonly Func<Task<string>> _token;
        public ConsulDiscoveryDelegatingHandler(string consulAddress
            , Func<Task<string>> token = null)
        {
            _consulClient = new ConsulClient(x =>
            {
                x.Address = new Uri(consulAddress);
            });
       //获取token的方法,可选参数
            _token = token;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request
            , CancellationToken cancellationToken)
        {
            var current = request.RequestUri;
            var cacheKey = $"service_consul_url_{current.Host }";
            try
            {
          //如果声明接口有验证头,在这里统一处理。
                var auth = request.Headers.Authorization;
                if (auth != null)
                {
                    if (_token == null) throw new ArgumentNullException(nameof(_token));

                    var tokenTxt = await _token();
                    request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, tokenTxt);
                }

          //服务地址缓存3秒
                var serverUrl = CacheManager.GetOrCreate<string>(cacheKey, entry =>
                {
                    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3);
                    return LookupService(current.Host);
                });

                request.RequestUri = new Uri($"{current.Scheme}://{serverUrl}{current.PathAndQuery}");
                return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                CacheManager.Remove(cacheKey);
                throw;
            }
            finally
            {
                request.RequestUri = current;
            }
        }

        private string LookupService(string serviceName)
        {
            //根据服务名获取服务地址
            var servicesEntry = _consulClient.Health.Service(serviceName, string.Empty, true).Result.Response;
            if (servicesEntry != null && servicesEntry.Any())
            {
          //目前只实现了随机轮询
                int index = new Random().Next(servicesEntry.Count());
                var entry = servicesEntry.ElementAt(index);
                return $"{entry.Service.Address}:{entry.Service.Port}";
            }
            return null;
        }
    }
}

如何使用

Refit的基本用法就不记录了,重点写Refit集成Consul如何写代码。

1、定义一个服务接口

public interface IAuthApi
{
    /// <summary>
    /// 不需要验证的接口
    /// </summary>
    /// <returns></returns>
    [Get("/sys/users")]
    Task <dynamic> GetUsers();

    /// <summary>
    /// 接口采用Bearer方式验证,Token在ConsulDiscoveryDelegatingHandler统一获取
    /// </summary>
    /// <returns></returns>
    [Get("/sys/session")]
    [Headers("Authorization: Bearer")]
    Task<dynamic> GetCurrentUserInfo();

    /// <summary>
    /// 接口采用Bearer方式验证,Token使用参数方式传递
    /// </summary>
    /// <returns></returns>
    [Get("/sys/session")]
    Task<dynamic> GetCurrentUserInfo([Header("Authorization")] string authorization);
}

2、在startup文件中注册Refit组件

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    //重试策略
    var retryPolicy = Policy.Handle<HttpRequestException>()
                            .OrResult<HttpResponseMessage>(response => response.StatusCode== System.Net.HttpStatusCode.BadGateway)
                            .WaitAndRetryAsync(new[]
                            {
                                TimeSpan.FromSeconds(1),
                                TimeSpan.FromSeconds(5),
                                TimeSpan.FromSeconds(10)
                            });
    //超时策略
    var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5);
    //隔离策略
    var bulkheadPolicy = Policy.BulkheadAsync<HttpResponseMessage>(10, 100);
    //回退策略
    //断路策略
    var circuitBreakerPolicy = Policy.Handle<Exception>()
                    .CircuitBreakerAsync(2, TimeSpan.FromMinutes(1));
    //注册RefitClient
    //用SystemTextJsonContentSerializer替换默认的NewtonsoftJsonContentSerializer序列化组件
    //如果调用接口是使用NewtonsoftJson序列化则不需要替换
    services.AddRefitClient<IAuthApi>(new RefitSettings(new SystemTextJsonContentSerializer()))
            //设置服务名称,andc-api-sys是系统在Consul注册的服务名
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://andc-api-sys"))
            //注册ConsulDiscoveryDelegatingHandler,
            .AddHttpMessageHandler(() =>
            {
                //http://12.112.75.55:8550是consul服务器的地址
                //() => Helper.GetToken() 获取token的方法,是可选参数,如果不需要token验证不需要传递。
                return new ConsulDiscoveryDelegatingHandler("http://12.112.75.55:8550", () => Helper.GetToken());
            })
            //设置httpclient生命周期时间,默认也是2分钟。
            .SetHandlerLifetime(TimeSpan.FromMinutes(2))
            //添加polly相关策略
            .AddPolicyHandler(retryPolicy)
            .AddPolicyHandler(timeoutPolicy)
            .AddPolicyHandler(bulkheadPolicy);
}

3、如何在controller中调用

public class HomeController : ControllerBase
{
   private readonly IAuthApi _authApi;
   private readonly string _token = Helper.GetToken().Result;

   /// <summary>
   /// RefitConsul测试
   /// </summary>
   /// <param name="authApi">IAuthApi服务</param>
   public HomeController(IAuthApi authApi)
   {
       _authApi = authApi;
   }

   [HttpGet]
   public async Task<dynamic> GetAsync()
   {
       //不需要验证的服务
       var result1 = await _authApi.GetUsers();

       //需要验证,token采用参数传递
       var result2 = await _authApi.GetCurrentUserInfo($"Bearer {_token}");

       //需要验证,token在ConsulDiscoveryDelegatingHandler获取。
       var result3 = await _authApi.GetCurrentUserInfo();

       return result3;
   }
}

源码地址