Spring Cloud OpenFeign 基本配置
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
FeignClientProperties类
该类在spring-cloud-openfeign-core-3.1.4.jar包中其定义了我们可以在Application.properties(yaml)中可以配置的内容。其源码如下
package org.springframework.cloud.openfeign;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import feign.Capability;
import feign.Contract;
import feign.ExceptionPropagationPolicy;
import feign.Logger;
import feign.QueryMapEncoder;
import feign.RequestInterceptor;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("feign.client")
public class FeignClientProperties {
private boolean defaultToProperties = true;
private String defaultConfig = "default";
private Map<String, FeignClientConfiguration> config = new HashMap<>();
/**
* Feign clients do not encode slash `/` characters by default. To change this
* behavior, set the `decodeSlash` to `false`.
*/
private boolean decodeSlash = true;
public boolean isDefaultToProperties() {
return defaultToProperties;
}
public void setDefaultToProperties(boolean defaultToProperties) {
this.defaultToProperties = defaultToProperties;
}
public String getDefaultConfig() {
return defaultConfig;
}
public void setDefaultConfig(String defaultConfig) {
this.defaultConfig = defaultConfig;
}
public Map<String, FeignClientConfiguration> getConfig() {
return config;
}
public void setConfig(Map<String, FeignClientConfiguration> config) {
this.config = config;
}
public boolean isDecodeSlash() {
return decodeSlash;
}
public void setDecodeSlash(boolean decodeSlash) {
this.decodeSlash = decodeSlash;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientProperties that = (FeignClientProperties) o;
return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig)
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash);
}
@Override
public int hashCode() {
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash);
}
/**
* Feign client configuration.
*/
public static class FeignClientConfiguration {
private Logger.Level loggerLevel;
private Integer connectTimeout;
private Integer readTimeout;
private Class<Retryer> retryer;
private Class<ErrorDecoder> errorDecoder;
private List<Class<RequestInterceptor>> requestInterceptors;
private Map<String, Collection<String>> defaultRequestHeaders;
private Map<String, Collection<String>> defaultQueryParameters;
private Boolean decode404;
private Class<Decoder> decoder;
private Class<Encoder> encoder;
private Class<Contract> contract;
private ExceptionPropagationPolicy exceptionPropagationPolicy;
private List<Class<Capability>> capabilities;
private Class<QueryMapEncoder> queryMapEncoder;
private MetricsProperties metrics;
private Boolean followRedirects;
public Logger.Level getLoggerLevel() {
return loggerLevel;
}
public void setLoggerLevel(Logger.Level loggerLevel) {
this.loggerLevel = loggerLevel;
}
public Integer getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Integer connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Integer getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(Integer readTimeout) {
this.readTimeout = readTimeout;
}
public Class<Retryer> getRetryer() {
return retryer;
}
public void setRetryer(Class<Retryer> retryer) {
this.retryer = retryer;
}
public Class<ErrorDecoder> getErrorDecoder() {
return errorDecoder;
}
public void setErrorDecoder(Class<ErrorDecoder> errorDecoder) {
this.errorDecoder = errorDecoder;
}
public List<Class<RequestInterceptor>> getRequestInterceptors() {
return requestInterceptors;
}
public void setRequestInterceptors(List<Class<RequestInterceptor>> requestInterceptors) {
this.requestInterceptors = requestInterceptors;
}
public Map<String, Collection<String>> getDefaultRequestHeaders() {
return defaultRequestHeaders;
}
public void setDefaultRequestHeaders(Map<String, Collection<String>> defaultRequestHeaders) {
this.defaultRequestHeaders = defaultRequestHeaders;
}
public Map<String, Collection<String>> getDefaultQueryParameters() {
return defaultQueryParameters;
}
public void setDefaultQueryParameters(Map<String, Collection<String>> defaultQueryParameters) {
this.defaultQueryParameters = defaultQueryParameters;
}
public Boolean getDecode404() {
return decode404;
}
public void setDecode404(Boolean decode404) {
this.decode404 = decode404;
}
public Class<Decoder> getDecoder() {
return decoder;
}
public void setDecoder(Class<Decoder> decoder) {
this.decoder = decoder;
}
public Class<Encoder> getEncoder() {
return encoder;
}
public void setEncoder(Class<Encoder> encoder) {
this.encoder = encoder;
}
public Class<Contract> getContract() {
return contract;
}
public void setContract(Class<Contract> contract) {
this.contract = contract;
}
public ExceptionPropagationPolicy getExceptionPropagationPolicy() {
return exceptionPropagationPolicy;
}
public void setExceptionPropagationPolicy(ExceptionPropagationPolicy exceptionPropagationPolicy) {
this.exceptionPropagationPolicy = exceptionPropagationPolicy;
}
public List<Class<Capability>> getCapabilities() {
return capabilities;
}
public void setCapabilities(List<Class<Capability>> capabilities) {
this.capabilities = capabilities;
}
public Class<QueryMapEncoder> getQueryMapEncoder() {
return queryMapEncoder;
}
public void setQueryMapEncoder(Class<QueryMapEncoder> queryMapEncoder) {
this.queryMapEncoder = queryMapEncoder;
}
public MetricsProperties getMetrics() {
return metrics;
}
public void setMetrics(MetricsProperties metrics) {
this.metrics = metrics;
}
public Boolean isFollowRedirects() {
return followRedirects;
}
public void setFollowRedirects(Boolean followRedirects) {
this.followRedirects = followRedirects;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientConfiguration that = (FeignClientConfiguration) o;
return loggerLevel == that.loggerLevel && Objects.equals(connectTimeout, that.connectTimeout)
&& Objects.equals(readTimeout, that.readTimeout) && Objects.equals(retryer, that.retryer)
&& Objects.equals(errorDecoder, that.errorDecoder)
&& Objects.equals(requestInterceptors, that.requestInterceptors)
&& Objects.equals(decode404, that.decode404) && Objects.equals(encoder, that.encoder)
&& Objects.equals(decoder, that.decoder) && Objects.equals(contract, that.contract)
&& Objects.equals(exceptionPropagationPolicy, that.exceptionPropagationPolicy)
&& Objects.equals(defaultRequestHeaders, that.defaultRequestHeaders)
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters)
&& Objects.equals(capabilities, that.capabilities)
&& Objects.equals(queryMapEncoder, that.queryMapEncoder) && Objects.equals(metrics, that.metrics)
&& Objects.equals(followRedirects, that.followRedirects);
}
@Override
public int hashCode() {
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors,
decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters,
defaultRequestHeaders, capabilities, queryMapEncoder, metrics, followRedirects);
}
}
/**
* Metrics configuration for Feign Client.
*/
public static class MetricsProperties {
private Boolean enabled = true;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MetricsProperties that = (MetricsProperties) o;
return Objects.equals(enabled, that.enabled);
}
@Override
public int hashCode() {
return Objects.hash(enabled);
}
}
}
配置类型
我们最需要配置的就是Map<String, FeignClientConfiguration> config他是一个Map类型这样的类型支持我们为每一个FeignClient配置不同的配置信息。
OpenFeign通过这个map的key值来区分不通Feign客户端的配置当然它也可以为没有任何配置的Feign客户端配置一个默认的配置 config.put(“default”,feignClientConfiguration)这个key值默认为“default”。默认的key值可以通过如下配置修改
# 默认的Feign配置名
feign.client.default-config= myDefault
## 配置默认的Feign客户端
feign.client.config.myDefault.xxxx =
feign.client.config.myDefault.yyyy =
## 配置名称为myCloud的Feign客户端(myCloud即为@FeignClient("myCloud")中的name属性值)
feign.client.config.myCloud.xxxx =
feign.client.config.myCloud.yyyy =
超时配置
# 连接超时时间(防止由于服务器处理时间长而阻塞调用单位ms
feign.client.config.myDefault.connect-timeout=5000
# 读取超时时间单位ms
feign.client.config.myDefault.read-timeout=5000
配置gzip压缩
feign.compression.request.enabled=true
feign.compression.response.enabled=true
开启gzip压缩有利于提高请求的响应效率
更详细的设置
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
配置日志打印
# 默认的Feign日志记录
feign.client.config.myDefault.logger-level = BASIC
日志记录的方式
- NONE, 没有日志记录(默认。
- BASIC, 只记录请求方法和URL以及响应状态码和执行时间。
- HEADERS, 记录基本信息以及请求和响应标头。
- FULL, 记录请求和响应的标头、正文和元数据。
以上都是枚举Logger.Level类型。
注Feign的日志依赖于sl4j这意味着我们还需要配置我们的FeignClient接口所在的类或者包需要加入我们的日志配置中。我们的示例是使用的Spring Boot 中的logback日志所以我在logback-spring.xml中加入了我的示例所在的包com.yyoo。并打印为debug日志
<logger name="com.yyoo" level="DEBUG" />
FULL日志的输出示例
---> POST http://myCloud/myCloud/conf/testJson HTTP/1.1
Content-Length: 26
Content-Type: application/json
{"name":"郭娟","age":96}
---> END HTTP (26-byte body)
<--- HTTP/1.1 200 (305ms)
connection: keep-alive
content-type: application/json
date: Sat, 14 Jan 2023 12:41:47 GMT
keep-alive: timeout=60
transfer-encoding: chunked
{"success":true,"msg":"请求成功","status":200,"bizCode":0,"content":{"name":"郭娟","age":96}}
<--- END HTTP (99-byte body)
Encoder、Decoder、Contract
我们知道OpenFeign底层是使用的Http客户端比如HttpClientHttpClient的请求方法如下
示例为使用Json对象请求
CloseableHttpClient client = HttpClientUtils.getHttpClient();
HttpPost httpPost = new HttpPost(url);
StringEntity stringEntity = new StringEntity(jsonStr,encoding);
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
// 设置响应头信息
httpPost.addHeader("Connection", "keep-alive");
httpPost.addHeader("Accept", "*/*");
httpPost.addHeader("Cache-Control", "max-age=0");
httpPost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
// 执行获得结果
CloseableHttpResponse response = client.execute(httpPost);
注以上为伪代码只是为了说明方便
整个过程我们的入参为json字符串响应为CloseableHttpResponse 对象。
再来看我们的示例
- 服务端
@RequestMapping("testJson")
public MyResponse testJson(@RequestBody UserBean bean){
return MyResponse.success("请求成功",bean);
}
MyResponse 是我们自定义的结果类
package com.yyoo.cloud.bean;
import lombok.Data;
/**
*
* 统一结果对象
*
*/
@Data
public class MyResponse<T> {
/**
* 应答成功或失败
*/
private boolean success;
/**
* 提示消息
*/
private String msg;
/**
* http 状态码
*/
private int status;
/**
* 业务状态码
*/
private int bizCode;
/**
* 返回结果对象
*/
private T content;
private MyResponse(){}
public static final <T> MyResponse<T> success(){
MyResponse<T> response = new MyResponse<T>();
response.setSuccess(true);
response.setStatus(200);
return response;
}
public static final <T> MyResponse<T> success(String msg){
MyResponse<T> response = success();
response.setMsg(msg);
return response;
}
public static final <T> MyResponse<T> success(T content){
MyResponse response = success();
response.setContent(content);
return response;
}
public static final <T> MyResponse<T> success(String msg,T content){
MyResponse response = success();
response.setContent(content);
response.setMsg(msg);
return response;
}
public static final <T> MyResponse<T> error(String msg){
MyResponse<T> response = new MyResponse<T>();
response.setMsg(msg);
response.setSuccess(false);
response.setStatus(500);
return response;
}
public static final <T> MyResponse<T> error(String msg,int bizCode){
MyResponse<T> response = error(msg);
response.setBizCode(bizCode);
return response;
}
public static final <T> MyResponse<T> error(String msg,T content){
MyResponse<T> response = error(msg);
response.setContent(content);
return response;
}
public static final <T> MyResponse<T> error(String msg,T content,int bizCode){
MyResponse<T> response = error(msg);
response.setContent(content);
response.setBizCode(bizCode);
return response;
}
}
- Feign客户端
@FeignClient("myCloud")
public interface MyClient {
@RequestMapping("/myCloud/conf/testJson")
MyResponse testJson(UserBean bean);
}
我们的入参是UserBean对象响应为MyResponse。我们这里可以直接使用UserBean入参和MyResponse响应的原因就是Encoder、Decoder
- Encoder在请求之前处理请求参数
- Decoder在响应之后处理响应结果
Encoder、Decoder在FeignClientsConfiguration中的默认配置
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters, customizers)));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass({"org.springframework.data.domain.Pageable"})
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return this.springEncoder(formWriterProvider, this.encoderProperties, customizers);
}
Encoder、Decoder作用过程源码解析
OpenFeign调用过程主要是SynchronousMethodHandler类中的invoke和executeAndDecode两个方法
- invoke执行远程调用
- executeAndDecode执行远程调用以及调用后的响应处理
SynchronousMethodHandler中invoke和executeAndDecode两个方法源码
@Override
public Object invoke(Object[] argv) throws Throwable {
// buildTemplateFromArgs 是RequestTemplate.Factory接口其有3个实现类
// BuildEncodedTemplateFromArgs、BuildFormEncodedTemplateFromArgs、BuildTemplateByResolvingArgs
// 均在ReflectiveFeign类中以静态内部类的形式实现每个类中都一个一个Encoder成员变量
// 我们的示例调用是使用的 BuildEncodedTemplateFromArgs 实现
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();// 重试接口
while (true) {
try {
return executeAndDecode(template, options);// 执行并处理响应
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e); // 出现异常重试
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {// 不是Logger.Level.NONE则打印日志
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
// 当前Decoder是我们的配置文件中配置的Decoder(我们当前示例为null
if (decoder != null)
return decoder.decode(response, metadata.returnType());
CompletableFuture<Object> resultFuture = new CompletableFuture<>();
// 使用asyncResponseHandler来处理响应(这里面也有Decoder是默认的Decoder
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime);
try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done");
return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause != null)
throw cause;
throw e;
}
}
上面对源码做了非常简单的一些说明有兴趣可以自己debug看看源码更详细的过程。
一般情况下我们使用默认的Encoder和Decoder就能满足我们的需求比如对象参数、文件上传等等都可以。
Contract 合约(契约
Contract的默认配置如下
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
boolean decodeSlash = this.feignClientProperties == null || this.feignClientProperties.isDecodeSlash();
return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
}
SpringMvcContract 作用就是支持Spring MVC的相关注解@PathVariable、@RequestMapping、@RequestParam等我们也一般不会更改此处就不做详细介绍了。
配置自定义的Encoder、Decoder、Contract
feign.client.config.myDefault.encoder= com.example.SimpleEncoder
feign.client.config.myDefault.decoder= com.example.SimpleDecoder
feign.client.config.myDefault.contract= com.example.SimpleContract
decode404配置
# 是否使用解码器解码404异常(为true的话客户端不会报异常而会返回一个status为404的json对象
feign.client.config.myDefault.decode404=true
如果Feign调用一个服务端不存在的地址则会出现以下结果
{
"success": false,
"msg": null,
"status": 404,
"bizCode": 0,
"content": null
}
decode404默认为false在实际情况下我们也应该设置为false特殊情况下在需要的时候才设置为true
设置默认请求参数和Header参数
# 默认请求参数 key = ddd
feign.client.config.myDefault.default-query-parameters.key = ddd
# 默认请求Header
feign.client.config.myDefault.default-request-headers.token = abcd
default-query-parameters、default-request-headers都是Map<String, Collection<String>>类型
每次Feign请求的时候都会带上我们配置的参数和header下面是对应的请求日志
---> POST http://myCloud/myCloud/conf/testJson?key=ddd HTTP/1.1
Content-Length: 26
Content-Type: application/json
token: abcd
{"name":"郭娟","age":96}
---> END HTTP (26-byte body)
<--- HTTP/1.1 200 (305ms)
connection: keep-alive
content-type: application/json
date: Sat, 14 Jan 2023 12:41:47 GMT
keep-alive: timeout=60
transfer-encoding: chunked
{"success":true,"msg":"请求成功","status":200,"bizCode":0,"content":{"name":"郭娟","age":96}}
<--- END HTTP (99-byte body)
可以看到url上会加上key = dddheader多了token参数。
OpenFeign的拦截器
配置拦截器
# 设置默认客户端的拦截器
feign.client.config.myDefault.request-interceptors[0]= com.yyoo.interceptor.MyInterceptors
MyInterceptors需实现RequestInterceptor接口
RequestInterceptor接口定义如下
public interface RequestInterceptor {
void apply(RequestTemplate template);
}
gzip压缩的实现其实就是拦截器来实现的分别在FeignAcceptGzipEncodingAutoConfiguration和FeignContentGzipEncodingAutoConfiguration中配置如FeignContentGzipEncodingAutoConfiguration源码如下
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(FeignClientEncodingProperties.class)
@ConditionalOnClass(Feign.class)
// The OK HTTP client uses "transparent" compression.
// If the content-encoding header is present it disable transparent compression
@ConditionalOnMissingBean(type = "okhttp3.OkHttpClient")
@ConditionalOnProperty("feign.compression.request.enabled")
@AutoConfigureAfter(FeignAutoConfiguration.class)
public class FeignContentGzipEncodingAutoConfiguration {
@Bean
public FeignContentGzipEncodingInterceptor feignContentGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
return new FeignContentGzipEncodingInterceptor(properties);
}
}
其使用FeignContentGzipEncodingInterceptor来实现。
BasicAuthRequestInterceptor
我们可以使用BasicAuthRequestInterceptor来实现最基本的账户认证。
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
OpenFeign的拦截器的主要作用就是在请求前设置一些特殊参数或header值