最近在使用 Feign 的时候碰到了请求超时的问题,借着这个事儿好好的梳理下与 Feign 相关的客户端,以及客户端的配置,此文可以作为《Feign 如何设置超时时间(connectionTimeout、readTimout)》的补充。
Feign 主要支持 3 种客户端,另外 1 个客户端作为对这 3 种客户端的包装,如下:
我是以这 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 个自动配置类:
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秒)
org.springframework.cloud.openfeign.FeignAutoConfiguration
可以理解 FeignAutoConfiguration 是一个被阉割了负载均衡能力的 FeignRibbonClientAutoConfiguration ,TA 通过内部类的方式(FeignRibbonClientAutoConfiguration 是通过 @import 注解)实现了对 Client.Default、 HttpClient(ApacheHttpClient)、OkHttpClient 3 种中客户端的支持
因为本篇的主要讲的是 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
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)
引入相关 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