背景
最近在使用 Feign 的时候碰到了请求超时的问题,借着这个事儿好好的梳理下与 Feign 相关的客户端,以及客户端的配置,此文可以作为《Feign 如何设置超时时间(connectionTimeout、readTimout)》的补充。
1、Feign 支持的客户端类型
Feign 主要支持 3 种客户端,另外 1 个客户端作为对这 3 种客户端的包装,如下:
- Client.Default
如果没有特别指定使用哪个客户端,则 Feign 将使用这个默认的客户端,该客户端内部使用 JDK 的 HttpURLConnnection 来处理 HTTP URL 的请求 - HttpClient(ApacheHttpClient)
Feign 将会通过 feign.httpclient.ApacheHttpClient(基于 Apache httpclient 开源组件) 来处理 HTTP URL 的请求 - OkHttpClient
Feign 将会通过 feign.okhttp.OkHttpClient(基于 Okhttp3 开源组件)来处理 HTTP URL 的请求 - LoadBalancerFeignClient
该类作为 Client.Default、ApacheHttpClient、OkHttpClient 的包装类而存在,内部使用 Ribbon 负载均衡算法获取 server 服务器,可以理解 Feign 是通过该类的 LoadBalancerFeignClient.execute(Request request, Options options) 方法将请求转发给真正的客户端(Client.Default、ApacheHttpClient、OkHttpClient)来完成对 HTTP URL 请求的处理
2、Feign 的 2 个自动配置类
我是以这 2 个自动配置类作为入口来认识 Feign 各种客户端配置的,下面将分别介绍下:
-
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration
通过上边的源代码截图,我们可以知道以下几件事情:1、通过 @ConditionalOnClass({ILoadBalancer.class, Feign.class}) 注解,我们可以知道该自动配置类执行的条件是:当类路径中存在 com.netflix.loadbalancer.ILoadBalancer 和 feign.Feign 该自动配置类就会执行
2、通过 @AutoConfigureBefore({FeignAutoConfiguration.class}) 注解,我们可以知道当 FeignRibbonClientAutoConfiguration 和 FeignAutoConfiguration 2 个自动配置类都满足执行条件,那么 FeignRibbonClientAutoConfiguration 将优先于 FeignAutoConfiguration 执行
3、通过注解 @Import({HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class}) 该自动配置类同时又引入进来另外 3 个自动配置类:
- DefaultFeignLoadBalancedConfiguration.class
负责加载一个包装了 Client.Default 的 LoadBalancerFeignClient 负载均衡客户端 - HttpClientFeignLoadBalancedConfiguration.class
负责加载一个包装了 HttpClient(ApacheHttpClient) 的 LoadBalancerFeignClient 负载均衡客户端 - OkHttpFeignLoadBalancedConfiguration.class
负责加载一个包装了 OkHttpClient 的 LoadBalancerFeignClient 负载均衡客户端 【我将会再下边的内容里重点介绍】
4、通过 @EnableConfigurationProperties({FeignHttpClientProperties.class}) 注解,我们可以知道与该自动配置类匹配的配置信息是 FeignHttpClientProperties.class,也就是项目(例如:annoroad-alpha)中 application.yml 配置文件中的如下内容:
feign: httpclient: max-connections: 200 # 连接池连接最大闲置数,缺省值是 200 time-to-live: 900 # 连接最大闲置时间,单位为秒,缺省值是 900秒(15分钟) connection-timeout: 2000 # 连接超时,单位为毫秒,缺省值是 2000毫秒(2秒)
- DefaultFeignLoadBalancedConfiguration.class
-
org.springframework.cloud.openfeign.FeignAutoConfiguration
可以理解 FeignAutoConfiguration 是一个被阉割了负载均衡能力的 FeignRibbonClientAutoConfiguration ,TA 通过内部类的方式(FeignRibbonClientAutoConfiguration 是通过 @import 注解)实现了对 Client.Default、 HttpClient(ApacheHttpClient)、OkHttpClient 3 种中客户端的支持
3、OkHttpFeignLoadBalancedConfiguration
因为本篇的主要讲的是 Feign 中的 Okhttp,所以这里我将以 OkHttpFeignLoadBalancedConfiguration 作为入口进行说明,首先先看下源代码截图:
通过上边的源代码截图,我们可以知道以下几件事情:
1、这里的有两个 OkHttpClient,一个是 Feign 对 okhttp3.OkHttpClient 的包装类(feign.okhttp.OkHttpClient),另外一个是原始的 okhttp3 的 OkHttpClient
2、通过 @ConditionalOnClass({OkHttpClient.class})、@ConditionalOnProperty({“feign.okhttp.enabled”}) 这两个注解,我们可以知道当类路径存在 feign.okhttp.OkHttpClient(换句话说就是 pom.xml 引入了 feign-okhttp 的 Jar 包),且项目中 application.yml 文件中的 feign.okhttp.enabled=true(例如:annoroad-alpha ),则该自动配置类执行
3、通过内部类 OkHttpFeignConfiguration 对 okhttp3.OkHttpClient 进行了初始化、加载
4、与 okhttp3.OkHttpClient 连接池(ConnectionPool)相关的参数最大空闲连接数、允许连接最大空闲时间,分别是通过 FeignHttpClientProperties 里的 maxConnections、timeToLive 参数来初始化的,这里的 maxConnections、timeToLive 分别等同于 application.yml 文件中的 feign.httpclient.max-conections、feign.httpclient.time-to-live
5、与 okhttp3.OkHttpClient 连接超时相关的设置,是通过 FeignHttpClientProperties 里的 connectionTimeout 参数来初始化的,这里的 connectionTimeout 等同于 application.yml 文件中的 feign.httpclient.connection-timeout
4、坑
-
readTimeout
如果你再细心点,可能会发现一个问题:
Q1:readTimeout 去哪里了呢?!
我们再回头看下 OkHttpFeignLoadBalancedConfiguration 类:
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); } }
实际上处理 HTTP URL 请求的是 feignClient(…) 方法中的 feign.okhttp.OkHttpClient.execute(…) 方法,源码如下:
public final class OkHttpClient implements Client { public Response execute(feign.Request input, Options options) throws IOException { okhttp3.OkHttpClient requestScoped; if (this.delegate.connectTimeoutMillis() == options.connectTimeoutMillis() && this.delegate.readTimeoutMillis() == options.readTimeoutMillis()) { 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(); } }
通过上边的代码片段我们可以发现,okhttp3.OkHttpClient 在执行真正的请求之前,会先拿 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 与已初始化的 okhttp3.OkHttpClient 中的 connectTimeout、readTimeout 进行对比,如果完全一致就不用说了。如果不一致的话,则会以 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 的值重新设置 okhttp3.OkHttpClient 中的 connectTimeout、readTimeout。这下我们就知道了,如果有需要自己指定一个 readTimeout,那么就要对 feign.Request.Options 中的 readTimeoutMillis 下手了,那问题又来了:
Q2:feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 是通过什么来设置的呢?!
实际上 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 来源于 org.springframework.cloud.openfeign.FeignClientProperties,如下图:
通过上边代码片段中的 @ConfigurationProperties(“feign.client”) 注解,我们可以知道 FeignClientProperties 对应项目中 application.yml 文件中的 feign.client 配置,如下:
feign: client: config: default: # 服务名,填写 default 为所有服务,或者指定某服务,例如:annoroad-beta connectTimeout: 10000 # 连接超时,10秒 readTimeout: 20000 # 读取超时,20秒
如果想要该设置生效(将指定的值赋值到 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis ),还必须满足一条:
Q3:connectTimeout 和 readTimeout 必须同时配置!!!!!!!!!!
至于为啥,看了下边的源代码就会明白了:
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { protected void configureUsingProperties( FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) { if (config == null) { return; } if (config.getLoggerLevel() != null) { builder.logLevel(config.getLoggerLevel()); } // ========= 此处,必须俩值都不为 null 才会替换新 options ======== if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { builder.options( new Request.Options( config.getConnectTimeout(), config.getReadTimeout())); }
巴拉巴拉说了这么多,简单的来说就是我们可以通过 同时(必须是同时!!!再次强调!!) 设置 application.yml 配置文件中的 feign.client.default.config.connectTimeout、feign.client.default.config.readTimeout 来使自己指定的 readTimeout 生效。
Ok,再向下深挖,又引发出新的问题:
Q4:feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis
到底是在什么时候被赋值为 FeignClientProperties 的 connectionTimout、readTimeout 的呢?!先不着急针对这个问题做解答,跟着我的思路从头来过:
1、首先是 Application 类中的 @EnableFeignClients 注解
2、让我进入到 @EnableFeignClients 注解 内部看看,如下图:
3、FeignClientsRegistrar 是个什么鬼,跟进源代码看看:
TA实现了接口 ImportBeanDefinitionRegistrar,实现了这个接口又有啥用呢?下面插播下 ImportBeanDefinitionRegistrar 接口的简介:1、 ImportBeanDefinitionRegistrar 类只能通过其他类 @Import 的方式来加载,通常是启动类或配置类。
2、使用@ Import,如果括号中的类是 ImportBeanDefinitionRegistrar 的实现类,则会调用接口方法(ImportBeanDefinitionRegistrar#registerBeanDefinitions),将其中要注册的类注册成 Bean。
3、实现该接口的类拥有注册 Bean 的能力。OK,原来是要自定义注册 Bean 的过程啊,那要注册什么 Bean 呢,让我们来看下 FeignClientsRegistrar 的源代码:
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
...
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
this.registerDefaultConfiguration(metadata, registry);
// !!! 注册所有带 @FeignClient 注解的 Bean
this.registerFeignClients(metadata, registry);
}
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
...
while(var17.hasNext()) {
...
while(var21.hasNext()) {
BeanDefinition candidateComponent = (BeanDefinition)var21.next();
if (candidateComponent instanceof AnnotatedBeanDefinition) {
...
// !!! 注册当前带 @FeignClient 注解的 Bean
this.registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition =
BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
...
BeanDefinitionHolder holder = new BeanDefinitionHolder(
beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
...
}
我们将主要注意力放在 FeignClientFactoryBean 身上,顾名思义 TA 就是 FeignClient 的工厂类(用来创建 FeignClient),而 TA 实现了 FactoryBean 接口,下面再插播一下 FactoryBean 的简介:
1、当配置文件中 的 class 属性配置的实现类是 FactoryBean 时,通过 getBean() 方法返回的不是 FactoryBean 本身,而是 FactoryBean#getObject() 方法所返回的对象,相当于 FactoryBean#getObject() 代理了 getBean() 方法
2、我们可以通过实现 FactoryBean 接口,自定义 Bean 的生成
下面继续看 FeignClientFactoryBean 的源代码:
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
public Object getObject() throws Exception {
return this.getTarget();
}
<T> T getTarget() {
FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
// !!! 生成一个 feign.Feign.Builder
Builder builder = this.feign(context);
...
}
protected Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory =
(FeignLoggerFactory)this.get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type);
Builder builder = ((Builder)this.get(context, Builder.class))
.logger(logger)
.encoder((Encoder)this.get(context, Encoder.class))
.decoder((Decoder)this.get(context, Decoder.class))
.contract((Contract)this.get(context, Contract.class));
// !!! Feign 配置信息
this.configureFeign(context, builder);
return builder;
}
protected void configureFeign(FeignContext context, Builder builder) {
// !!! 原来是这个使用获取的 FeignClientProperties
FeignClientProperties properties =
(FeignClientProperties)this.applicationContext.getBean(FeignClientProperties.class);
if (properties != null) {
if (properties.isDefaultToProperties()) {
this.configureUsingConfiguration(context, builder);
this.configureUsingProperties(
(FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()),
builder);
this.configureUsingProperties(
(FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
} else {
this.configureUsingProperties(
(FeignClientConfiguration)properties.getConfig().get(properties.getDefaultConfig()),
builder);
this.configureUsingProperties(
(FeignClientConfiguration)properties.getConfig().get(this.contextId), builder);
this.configureUsingConfiguration(context, builder);
}
} else {
this.configureUsingConfiguration(context, builder);
}
}
protected void configureUsingConfiguration(FeignContext context, Builder builder) {
...
Options options = (Options)this.getOptional(context, Options.class);
if (options != null) {
builder.options(options);
}
...
}
protected void configureUsingProperties(FeignClientConfiguration config, Builder builder) {
if (config != null) {
...
if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
builder.options(new Options(config.getConnectTimeout(), config.getReadTimeout()));
}
...
}
}
}
通过上边代码中的最后两个方法 configureUsingConfiguration、configureUsingProperties,我们可以看到原来是在这里 FeignClientProperties 与 feign.Request.Options 终于有了交集,而后又将 feign.Request.Options 作为属性值赋值到了 feign.Feign.Builder 中。
OK,至此也回答了上边的问题(Q4)…
稍等下,还没玩完 (哈哈哈 :>),既然我们提到 feign.Feign.Builder ,那就顺道再聊聊 TA 是如何创建 feign.Feign 的吧,来看源代码吧:
public abstract class Feign {
...
public static class Builder {
...
public Feign build() {
Factory synchronousMethodHandlerFactory = new Factory(
this.client, this.retryer, this.requestInterceptors,
this.logger, this.logLevel, this.decode404,
this.closeAfterDecode, this.propagationPolicy);
ParseHandlersByName handlersByName = new ParseHandlersByName(
this.contract, this.options, this.encoder,
this.decoder, this.queryMapEncoder, this.errorDecoder,
synchronousMethodHandlerFactory);
return new ReflectiveFeign(
handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
}
}
}
ReflectiveFeign.ParseHandlersByName 类在 ReflectiveFeign.ParseHandlersByName#apply 方法中创建了 feign.SynchronousMethodHandler,通过 feign.SynchronousMethodHandler 的 feign.SynchronousMethodHandler#invoke、feign.SynchronousMethodHandler#executeAndDecode 方法将 feign.Request.Options 作为参数传给 feign.Client 接口的具体实现类(例如:feign.okhttp.OkHttpClient)的 feign.Client#execute 方法,然后就是上文提到过在请求 okhttp3.OkHttpClient#execute 之前完成 okhttp3.OkHttpClient(connectTimeout、readTimeout) 与 入参 feign.Request.Options(connectTimeoutMillis、readTimeoutMillis)的对比,然后根据结果判断是否需要重置 okhttp3.OkHttpClient 的 connectTimeout、readTimeout。
-
connectTimeout
还记得之前有说过我们可以通过设置项目中(例如:annoroad-alpha) application.yml 文件中的 feign.httpclient.connection-timeout 来设置 okhttp3.OkHttpClient 的 connectionTimeout 吗?!这句话充其量只能说对了 10%,实际上这个配置只对 okhttp3.OkHttpClient 初始化阶段有影响,当我们真正要处理 HTTP URL 请求的时候,最主要的还是依据 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis(如果初始化的 okhttp3.OkHttpClient 实例中的 connectTimeoutMillis、readTimeoutMillis 与 feign.Request.Options 中的不一致,那么 feign.Request.Options 中的 connectTimeoutMillis、readTimeoutMillis 将会覆盖 okhttp3.OkHttpClient 实例中的 connectTimeoutMillis、readTimeoutMillis)
5、如何在 springboot 项目中应用 Okhttp
-
引入相关 starter
在 pom.xml 文件中加入 spring-cloud-starter-netflix-ribbon、feign-okhttp 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <version>2.1.1.RELEASE</version> </dependency> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> <version>10.7.4</version> </dependency>
(注:这里我引用的是 spring-cloud-starter-netflix-eureka-client 而非直接引用 spring-cloud-starter-netflix-ribbon,之所以也可以是因为 spring-cloud-starter-netflix-eureka-client 本身依赖 spring-cloud-starter-netflix-eureka-client)
-
配置
在 application.yml 文件中增加如下配置:
feign: client: config: default: # 服务名,填写 default 为所有服务,或者指定某服务,例如:annoroad-beta connectTimeout: 10000 # 连接超时,10秒 readTimeout: 20000 # 读取超时,20秒 httpclient: enabled: false # 关闭 ApacheHttpClient max-connections: 50 # 连接池连接最大连接数(缺省值 200) time-to-live: 600 # 连接最大闲置时间,单位为秒,600秒==10分钟(缺省值为 900秒==15分钟) okhttp: enabled: true # 开启 okhttp