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() {@Overridepublic 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 {@Overridepublic 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;@Overrideprotected 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;@Overrideprotected 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 相关
4.3 feign.compression.request 相关
5 连接池
关于 OpenFeign 的请求,我们知道它是基于 Http 的,并且默认的情况下,它是不开启连接池的:
@Configuration(proxyBeanMethods = false) class DefaultFeignLoadBalancedConfiguration {@Bean// 当没有别的 Client 的话走这里,也就是默认走这里 @ConditionalOnMissingBeanpublic 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 {...@Overridepublic 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) {// 默认走这里创建 HttpClientthis.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);}@Beanpublic 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;}@PreDestroypublic 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 小结
好啦,本节就看到这里,有理解不对的地方欢迎指点。