Spring Cloud OpenFeign 基本配置


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 对象。

再来看我们的示例

  1. 服务端
    @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;
    }
}

  1. 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值

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring