【OpenFeign 】OpenFeign 的常用配置以及连接池的配置方式

1  前言

上节我们看了下 OpenFeign 里的重试,在从源码的角度看它的执行原理的时候,又意外的遇到了一个【OpenFeign 】OpenFeign 下未开启重试,服务却被调用了两次 的问题的分析,后面我们又看了重试器的入场和执行时机,那么本节我们看看 OpenFeign 的一些常用配置,以及全局配置和想对某个 Feign 单独配置的方法。

官网地址以及官网的配置,大家也可以去看看。

2  环境准备

在前面的 Feign 下,我增加了一个 StockFeign,这样来测测单独针对某个 Feign 的配置:

3  配置相关

3.1  配置优先级

在了解配置之前,要先知道配置的关系。

配置也是分层次的,比如有一个全局的配置,有可以针对某个 Feign 单独的配置,配置取值优先级采用的是就近策略,也就是这个 Feign 有自己的配置了,就用自己的,没有的话就用默认的。

这块逻辑体现在:

// FeignClientFactoryBean 在初始化 Feign.Builder 的时候
protected void configureFeign(FeignContext context, Feign.Builder builder) {
    FeignClientProperties properties = beanFactory != null
            ? beanFactory.getBean(FeignClientProperties.class)
            : applicationContext.getBean(FeignClientProperties.class);
    FeignClientConfigurer feignClientConfigurer = getOptional(context,
            FeignClientConfigurer.class);
    setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
    if (properties != null && inheritParentContext) {
        if (properties.isDefaultToProperties()) {
            configureUsingConfiguration(context, builder);
            // 先设置 default 的
            configureUsingProperties(
                    properties.getConfig().get(properties.getDefaultConfig()),
                    builder);
            // 再设置自己特有的 properties.getConfig().get(contextId)
            configureUsingProperties(properties.getConfig().get(contextId), builder);
        }
        else {
            // 先设置 default 的
            configureUsingProperties(
                    properties.getConfig().get(properties.getDefaultConfig()),
                    builder);
            // 再设置自己特有的 properties.getConfig().get(contextId)
            configureUsingProperties(properties.getConfig().get(contextId), builder);
            configureUsingConfiguration(context, builder);
        }
    }
    else {
        configureUsingConfiguration(context, builder);
    }
}

配置设置参考:

# 设置默认的配置的名称 默认是 default 一般我们都不会动它
feign.client.default-config = default
# 设置默认的属性值
# 格式:feign.client.config.default.属性名 = 属性值
feign.client.config.default.read-timeout=1000
# 设置某个 Feign 特有的 contextId 就是@FeignClient 的 contextId,contextId为空的话取 name属性的值
feign.client.config.feign的contextId.read-timeout=1000

3.2  配置的相关类

(1)FeignClientProperties

// Feign客户端的基础配置选项,比如日志级别、重试策略、编码器和解码器的选择等。
@ConfigurationProperties("feign.client")
public class FeignClientProperties {

(2)FeignClientEncodingProperties

// 请求压缩相关 mimeTypes 支持的mime类型默认text/xml,application/xml,application/json  minRequestSize 边界超过多少进行请求压缩 默认2048
@ConfigurationProperties("feign.compression.request")
public class FeignClientEncodingProperties {

(3)FeignHttpClientProperties

// 这个配置类可能包含了一系列与HTTP客户端相关配置,如连接池大小、连接超时时间、读取超时时间等。使用Apache HttpClient或其他HTTP客户端时,这类配置是非常有用的。例如,当使用Apache HttpClient作为Feign的HTTP客户端时,可以通过此类配置来优化连接管理和性能。
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {

(4)HTTP 连接池相关的两个

# feign.httpclient.enabled
# feign.okhttp.enabled
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
class HttpClientFeignLoadBalancedConfiguration {
@ConditionalOnProperty("feign.okhttp.enabled")
class OkHttpFeignLoadBalancedConfiguration {
// 熔断相关 feign.hystrix.enabled
protected static class HystrixFeignConfiguration {
    @ConditionalOnProperty(name = "feign.hystrix.enabled")
    public Feign.Builder feignHystrixBuilder() {
        return HystrixFeign.builder();
    }
}

4  常用配置

4.1  feign.client 相关的

设置默认配置的名称,默认就是 default 这个不建议设置,就用默认的即可。

feign.client.default-config = default

feign.client 可以设置默认的配置,也可以对某个 feign 设置最后都是保存在 FeignClientProperties 的 config 属性中。

private Map<String, FeignClientConfiguration> config = new HashMap<>();

设置默认的:feign.client.config.default.属性 = 属性值

设置某个的:feign.client.config.contextId.属性 = 属性值

FeignClientConfiguration 是 FeignClientProperties 的子类,是针对 feignClient 可以设置的一些属性,我们看一些常见的:

4.1.1  日志

(1)日志相关的:loggerLevel  Level有 NONE、BASIC、HEADERS、FULL 四个等级,不设置的话默认为 null

比如:feign.client.config.default.logger-level = full

它的作用点就是在执行过程中,判断当前的日志级别,来打印相应的信息:

// 摘自 SynchronousMethodHandler
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);
    // 判断日志级别 打印相应信息
    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }
    try {
      response = client.execute(request, options);
      ...
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      ...
}

4.1.2 连接、响应超时时间

connectTimeout 连接超时时间、readTimeout 响应超时时间(单位时间:毫秒)比如:

# 设置默认的响应时间 1 秒
feign.client.config.default.read-timeout=1000
# 设置 stockFeign 的响应时间 5 秒
feign.client.config.stockFeign.read-timeout=5000

它的作用点在:

在初始化 Feign.Builder 的时候取出配置的连接、响应超时时间并用 Request.Options 封装起来:

// Request.Options()
public Options() {
  // 默认连接超时 10 秒,响应超时 60 秒
  this(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
}
// 默认值来自于 Request.Options()
private int readTimeoutMillis = new Request.Options().readTimeoutMillis();
private int connectTimeoutMillis = new Request.Options().connectTimeoutMillis();
private boolean followRedirects = new Request.Options().isFollowRedirects();
// 如果配置了,取配置的没有的话取 Request.Options() 上边默认的
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis;
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects;
// 构建 new Request.Options
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS,
                readTimeoutMillis, TimeUnit.MILLISECONDS, followRedirects));

默认情况下请求是通过 HttpURLConnection 发送的,根据你的 Request.Options() 建立请求:

@Override
public Response execute(Request request, Options options) throws IOException {
    HttpURLConnection connection = convertAndSend(request, options);
    return convertResponse(connection, request);
}
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
    ...
    connection.setConnectTimeout(options.connectTimeoutMillis());
    connection.setReadTimeout(options.readTimeoutMillis());
}

而对于 ApacheHttpClient 或者 okHttp 他们都是由 FeignHttpClientProperties即(feign.httpclient)来管理。

你看 HttpClient,最大连接数、连接超时等都是从 FeignHttpClientProperties 获取的:

@Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(
        ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
        FeignHttpClientProperties httpClientProperties) {
    final HttpClientConnectionManager connectionManager = connectionManagerFactory
            .newConnectionManager(httpClientProperties.isDisableSslValidation(),
                    httpClientProperties.getMaxConnections(),
                    httpClientProperties.getMaxConnectionsPerRoute(),
                    httpClientProperties.getTimeToLive(),
                    httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
    this.connectionManagerTimer.schedule(new TimerTask() {
        @Override
        public void run() {
            connectionManager.closeExpiredConnections();
        }
    }, 30000, httpClientProperties.getConnectionTimerRepeat());
    return connectionManager;
}
private CloseableHttpClient createClient(HttpClientBuilder builder,
        HttpClientConnectionManager httpClientConnectionManager,
        FeignHttpClientProperties httpClientProperties) {
    // 从 FeignHttpClientProperties 获取 连接超时时间 默认 2秒
    // public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;
    RequestConfig defaultRequestConfig = RequestConfig.custom()
            .setConnectTimeout(httpClientProperties.getConnectionTimeout())
            .setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
    CloseableHttpClient httpClient = builder
            .setDefaultRequestConfig(defaultRequestConfig)
            .setConnectionManager(httpClientConnectionManager).build();
    return httpClient;
}

你看 okHttp 一样的都是从 FeignHttpClientProperties 获取的:

@Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
        ConnectionPool connectionPool,
        FeignHttpClientProperties httpClientProperties) {
    Boolean followRedirects = httpClientProperties.isFollowRedirects();
    Integer connectTimeout = httpClientProperties.getConnectionTimeout();
    this.okHttpClient = httpClientFactory
            .createBuilder(httpClientProperties.isDisableSslValidation())
            .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
            .followRedirects(followRedirects).connectionPool(connectionPool).build();
    return this.okHttpClient;
}

有一点比较好奇的是 FeignHttpClientProperties 这些没有响应超时时间,连接池不能设置超时时间么?

其实连接池的响应超时时间、连接超时时间都是从 Request.Options 获取的,我们来看下:

ApacheHttpClient、OkHttpClient 都实现了 feign core包里的 Client 接口:

public interface Client {
  Response execute(Request request, Options options) throws IOException;
}

对于 ApacheHttpClient

public final class ApacheHttpClient implements Client {
  @Override
  public Response execute(Request request, Request.Options options) throws IOException {
    HttpUriRequest httpUriRequest;
    try {
      httpUriRequest = toHttpUriRequest(request, options);
      ...
    }
  }
}
HttpUriRequest toHttpUriRequest(Request request, Request.Options options)
    throws URISyntaxException {
  RequestBuilder requestBuilder = RequestBuilder.create(request.httpMethod().name());
  // per request timeouts 从 Request.Options 获取连接超时和响应超时
  RequestConfig requestConfig =
      (client instanceof Configurable ? RequestConfig.copy(((Configurable) client).getConfig())
          : RequestConfig.custom())
              .setConnectTimeout(options.connectTimeoutMillis())
              .setSocketTimeout(options.readTimeoutMillis())
              .build();
  requestBuilder.setConfig(requestConfig);
  URI uri = new URIBuilder(request.url()).build();
  ...
}

对于 OkHttpClient

public Response execute(feign.Request input, Options options) throws IOException {
    okhttp3.OkHttpClient requestScoped;
    // 也是从 Options 来获取的
    if (this.delegate.connectTimeoutMillis() == options.connectTimeoutMillis() && this.delegate.readTimeoutMillis() == options.readTimeoutMillis() && this.delegate.followRedirects() == options.isFollowRedirects()) {
        requestScoped = this.delegate;
    } else {
        requestScoped = this.delegate.newBuilder().connectTimeout((long)options.connectTimeoutMillis(), TimeUnit.MILLISECONDS).readTimeout((long)options.readTimeoutMillis(), TimeUnit.MILLISECONDS).followRedirects(options.isFollowRedirects()).build();
    }
    Request request = toOkHttpRequest(input);
    okhttp3.Response response = requestScoped.newCall(request).execute();
    return toFeignResponse(response, input).toBuilder().request(input).build();
}

我这里拿 ApacheHttpClient 调试如下:

虽然 ApacheHttpClient 默认连接超时是 2秒,但是由于 Options 默认是 10秒,所以 ApacheHttpClient 被重置为了 10秒,并且我配置的响应超时 1秒也生效了:

所以不管对于连接池方式的还是默认的 HttpURLConnection,连接超时或者响应超时的配置都可以通过 feign.client.config 来做:

feign.client.config.default.read-timeout=1000
feign.client.config.default.connect-timeout=1000
或者
feign.client.config.某个contextId.read-timeout=1000
feign.client.config.某个contextId.connect-timeout=1000

4.1.3 Retryer 重试器

重试器我们这里就不看了吧,之前都看过了。

4.1.4 RequestInterceptor 拦截器

OpenFeign 在执行请求的时候,给我们提供了一个拦截器,来做一些自定义的处理。

public interface RequestInterceptor {
  void apply(RequestTemplate template);
}

它的执行时机如图,是在 SynchronousMethodHandler 的 executeAndDecode 执行请求时的 targetRequest 方法中执行拦截器的:

Request targetRequest(RequestTemplate template) {
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}

它的场景主要有:

  • 比如我们服务之间互相调用,要传递用户信息,通过从上下文取到当前用户标志塞到 RequestTemplate 的 header 中,下游服务再从 Header中解析获得。
  • 比如我们要看服务调用时的一些请求参数等信息,可以通过拦截器打印。
  • 比如服务之间的链路监控,也是通过将 TraceId 放置到 header 中,跟第一点方式原理类似。

4.1.5 默认请求头、请求参数

defaultRequestHeaders 默认的请求头、defaultQueryParameters 默认的路径参数

他俩的实现原理都是通过拦截器来实现的:

protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) {
    ...
    if (Objects.nonNull(config.getDefaultRequestHeaders())) {
        // lambda 放置一个拦截器
        builder.requestInterceptor(requestTemplate -> requestTemplate
                .headers(config.getDefaultRequestHeaders()));
    }
    if (Objects.nonNull(config.getDefaultQueryParameters())) {
        // lambda 放置一个拦截器
        builder.requestInterceptor(requestTemplate -> requestTemplate
                .queries(config.getDefaultQueryParameters()));
    }
    ...
}

4.1.6 编码器、解码器

encode 编码:就是在请求发出之前对参数进行编码

decode 解码:在接收到结果数据后对其进行解码

说实话,这块还真没研究过,这里就看一下这两者的执行时机:

4.1.6.1 encoder 编码时机

我们的方法处理器 SynchronousMethodHandler 有一个这样的属性:

private final RequestTemplate.Factory buildTemplateFromArgs;

它来源于 ReflectiveFeign 解析你的 Feign 的方法的时候会根据你方法请求方式以及参数来创建不同的 BuildTemplateByResolvingArgs(它实现了 RequestTemplate.Factory),并且放置编码器在里边:

public Map<String, MethodHandler> apply(Target target) {
  List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
  Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
  // 遍历每个方法的原始信息
  for (MethodMetadata md : metadata) {
    BuildTemplateByResolvingArgs buildTemplate;
    // 当是存在 form 表单形式的提交
    if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
      buildTemplate =
          new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    // 其次是有请求体的时候
    } else if (md.bodyIndex() != null) {
      buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
    } else {
    // 剩余的走这里
      buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
    }
    ...
  }
  return result;
}

先看下类图关系:

然后 SynchronousMethodHandler 在执行的第一步就是构建 RequestTemplate,就会调用 buildTemplateFromArgs 的 create 方法:

@Override
public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    ...
}

接着就会先进入父类 BuildTemplateByResolvingArgs 的 create 方法:

@Override
public RequestTemplate create(Object[] argv) {
    ...
    // 解析
    RequestTemplate template = resolve(argv, mutable, varBuilder);
    ...
}

解析方法 resolve 就会进入上边的两个编码过程:

// BuildFormEncodedTemplateFromArgs 的:
private static class BuildFormEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
    private final Encoder encoder;
    @Override
    protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
      Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
      for (Entry<String, Object> entry : variables.entrySet()) {
        if (metadata.formParams().contains(entry.getKey())) {
          formVariables.put(entry.getKey(), entry.getValue());
        }
      }
      try {
        encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
      } catch (EncodeException e) {
        throw e;
      } catch (RuntimeException e) {
        throw new EncodeException(e.getMessage(), e);
      }
      return super.resolve(argv, mutable, variables);
    }
  }
}
// BuildEncodedTemplateFromArgs 的:
private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {
  private final Encoder encoder;
  @Override
  protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
    Object body = argv[metadata.bodyIndex()];
    checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
    try {
      encoder.encode(body, metadata.bodyType(), mutable);
    } catch (EncodeException e) {
      throw e;
    } catch (RuntimeException e) {
      throw new EncodeException(e.getMessage(), e);
    }
    return super.resolve(argv, mutable, variables);
  }
}

4.1.6.1 decoder 解码时机

解码就是在 SynchronousMethodHandler 的 executeAndDecode 执行方法中,当响应结果回来后:

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);
    Response response;
    try {
        // 发送请求
        response = client.execute(request, options);
        response = response.toBuilder()
            .request(request)
            .requestTemplate(template)
            .build();
    } catch (IOException e) {
        throw errorExecuting(request, e);
    }
    // 解码
    if (decoder != null)
        return decoder.decode(response, metadata.returnType());
        ...
}

4.2  feign.httpclient 相关

feign.httpclient 其实我们上边看到了,它是作用给开启连接池的时候用到的,我们这里简单看下都能配置哪些内容以及每个的默认值:

# 开启 httpclient连接池 前提要引入依赖才能生效 默认 false
feign.httpclient.enabled = false
# 连接超时时间  默认 20秒
feign.httpclient.connection-timeout = 2000
# 多久检查一次连接池中的空闲连接是否仍然可用  默认30秒
feign.httpclient.connection-timer-repeat = 3000
# 用于控制Feign客户端在遇到HTTP重定向(如301、302状态码)时是否自动跟随重定向  默认 true
feign.httpclient.follow-redirects = true
# 用于限制Feign客户端能够建立的总的并发连接数 默认 200
feign.httpclient.max-connections = 200
# 这个配置项用于限制针对特定目标服务器(路由)的并发连接数 默认 50
# 路由是指到特定目标服务器的连接路径,通常对应于一个特定的主机和端口组合。
feign.httpclient.max-connections-per-route = 50
# 用于定义连接在连接池中闲置多长时间后会被关闭 默认 900
feign.httpclient.time-to-live = 900
# 配合上边的闲置单位 默认秒
feign.httpclient.time-to-live-unit = seconds
# 用于控制是否禁用SSL证书验证。当使用HTTPS协议时,默认情况下,Feign客户端会验证服务器的SSL证书以确保通信的安全性
# 然而,在某些开发或测试环境中,可能需要禁用这一验证,以绕过自签名证书或其他非正式证书的验证问题
# 默认 false
feign.httpclient.disable-ssl-validation = false

还记得上边的连接池连接超时时间的优先级么,比如我设置了如下:

feign.client.config.default.connect-timeout=1000
feign.httpclient.connection-timeout=2000

那连接超时时间时多少呢?应该是 10秒 对吧,虽然连接池设置的是20秒,但最后会被 Feign,Builder 里的 Request.Options 覆盖为 10秒,我们调试看看:

4.3  feign.compression.request 相关

# 是否开启请求压缩 默认 false
feign.compression.request.enabled = false
# 请求压缩支持的 mime 类型
feign.compression.request.mime-types = text/xml,application/xml,application/json
# 设置最小请求大小,只有当请求体的大小超过这个阈值时才启用压缩 默认 2048
feign.compression.request.min-request-size = 2048
# 设置是否启用响应压缩 如果启用,Feign客户端会处理来自服务器的压缩响应 默认 false
feign.compression.response.enabled = false
# 设置响应开启 gzip 解码器 默认 false
feign.compression.response.useGzipDecoder = false

5  连接池

关于 OpenFeign 的请求,我们知道它是基于 Http 的,并且默认的情况下,它是不开启连接池的:

@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {
    @Bean
    // 当没有别的 Client 的话走这里,也就是默认走这里
    @ConditionalOnMissingBean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
            SpringClientFactory clientFactory) {
        // 客户端默认用的是 Client.Default (Client 是 feign 核心包里的 Default 是默认的实现)
        return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
                clientFactory);
    }
}
// Client.Default
class Default implements Client {
    ...
    @Override
    public Response execute(Request request, Options options) throws IOException {
        // 每次请求都是用的 HttpURLConnection 当没有keep-alive的情况下,其实每次请求都会经历建立连接发送接收数据断开连接 影响性能
        HttpURLConnection connection = convertAndSend(request, options);
        return convertResponse(connection, request);
    }
}

5.1  Apache-HttpClient

如果要开启 Apache 的 HttpClient 作为 HTTP 客户端,首先引入依赖:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

并且开启配置即可:

feign.httpclient.enabled = true

原理来源于两个配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Conditional(HttpClient5DisabledConditions.class)
// 引入 HttpClientFeignConfiguration
@Import(HttpClientFeignConfiguration.class)
class HttpClientFeignLoadBalancedConfiguration {
    @Bean
    @ConditionalOnMissingBean(Client.class)
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
            SpringClientFactory clientFactory, HttpClient httpClient) {
        // httpClient 来源于下面的配置类
        ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
        return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
    }
}
// HttpClientFeignConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CloseableHttpClient.class)
public class HttpClientFeignConfiguration {
    @Bean
    @ConditionalOnProperty(value = "feign.compression.response.enabled",
            havingValue = "false", matchIfMissing = true)
    public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
            HttpClientConnectionManager httpClientConnectionManager,
            FeignHttpClientProperties httpClientProperties) {
        // 默认走这里创建 HttpClient
        this.httpClient = createClient(httpClientFactory.createBuilder(),
                httpClientConnectionManager, httpClientProperties);
        return this.httpClient;
    }

    private CloseableHttpClient createClient(HttpClientBuilder builder,
            HttpClientConnectionManager httpClientConnectionManager,
            FeignHttpClientProperties httpClientProperties) {
        RequestConfig defaultRequestConfig = RequestConfig.custom()
                .setConnectTimeout(httpClientProperties.getConnectionTimeout())
                .setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
        CloseableHttpClient httpClient = builder
                .setDefaultRequestConfig(defaultRequestConfig)
                .setConnectionManager(httpClientConnectionManager).build();
        return httpClient;
    }
    ...
}

5.2  OkHttp

如果要开启 okHttp 作为 HTTP 客户端,也是先引入依赖:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

并且开启配置即可:

feign.okhttp.enabled = true

原理也是来源于两个配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty("feign.okhttp.enabled")
// 引入 OkHttpFeignConfiguration
@Import(OkHttpFeignConfiguration.class)
class OkHttpFeignLoadBalancedConfiguration {
    @Bean
    @ConditionalOnMissingBean(Client.class)
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
            SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
        OkHttpClient delegate = new OkHttpClient(okHttpClient);
        return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
    }

}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
public class OkHttpFeignConfiguration {

    private okhttp3.OkHttpClient okHttpClient;

    @Bean
    @ConditionalOnMissingBean(ConnectionPool.class)
    public ConnectionPool httpClientConnectionPool(
            FeignHttpClientProperties httpClientProperties,
            OkHttpClientConnectionPoolFactory connectionPoolFactory) {
        Integer maxTotalConnections = httpClientProperties.getMaxConnections();
        Long timeToLive = httpClientProperties.getTimeToLive();
        TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
        return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
    }

    @Bean
    public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
            ConnectionPool connectionPool,
            FeignHttpClientProperties httpClientProperties) {
        Boolean followRedirects = httpClientProperties.isFollowRedirects();
        Integer connectTimeout = httpClientProperties.getConnectionTimeout();
        this.okHttpClient = httpClientFactory
                .createBuilder(httpClientProperties.isDisableSslValidation())
                .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                .followRedirects(followRedirects).connectionPool(connectionPool).build();
        return this.okHttpClient;
    }

    @PreDestroy
    public void destroy() {
        if (this.okHttpClient != null) {
            this.okHttpClient.dispatcher().executorService().shutdown();
            this.okHttpClient.connectionPool().evictAll();
        }
    }

}

另外要注意的是,如果你的依赖中既包含 httpClient 又包含 okHttpClient 的话,默认是用的 httpClient

即使你设置了 feign.okhttp.enabled = true,也没用还是会用 httpClient,我调试发现是这样的。

@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
class HttpClientFeignLoadBalancedConfiguration {

6  小结

好啦,本节就看到这里,有理解不对的地方欢迎指点。

posted @ 2024-09-12 20:50  酷酷-  阅读(887)  评论(0编辑  收藏  举报