Spring|Spring Cloud Feign组件

概述 【Spring|Spring Cloud Feign组件】Feign 是Netflix开源的一个声明式的Http 客户端,它的目的就是让Web Service基于Http的远程调用变得更加简单。
Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign 默认集成了 Ribbon,Nacos 也很好的兼容了 Feign,默认实现了负载均衡的效果。

  • Feign 采用的是基于接口的注解
  • Feign 整合了 ribbon
快速入门 1、引入maven依赖:
org.springframework.boot spring-boot-starter-parent 2.1.10.RELEASE org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign io.github.openfeign feign-httpclient org.springframework.cloud spring-cloud-dependencies Greenwich.SR3 pom import

2、通过@EnableFeignClients注解开启Feign功能
@SpringBootApplication @EnableFeignClients public class OrderApiApplication { public static void main(String[] args) { SpringApplication.run(OrderApiApplication.class, args); } }

3、创建Feign接口,通过 @FeignClient("服务名") 注解来指定调用哪个服务
@FeignClient(value="https://www.it610.com/article/price-center") public interface PriceCenterFeignClient {@GetMapping(value = "https://www.it610.com/prices/{id}") PriceInfo getPrice(@PathVariable("id") Integer id); }

Feign的组成
接口 作用 默认值
Feign.Builder Feign的入口 Feign.Builder
Client Feign底层用什么去请求 和Ribbon配合时:LoadBalancerFeignClient
不和Ribbon配合时:Feign.Client.Default
Contract 契约,注解支持 SpringMvcContract
Encoder 编码器,用于将对象转换成HTTP请求消息体 SpringEncoder
Decoder 解码器,将响应消息体转换成对象 ResponseEntityDecoder
Logger 日志管理器 Slf4jLogger
RequestInterceptor 用于为每个请求添加通用逻辑 需要手动实现
Feign的配置自定义 细粒度自定义Feign的日志级别配置
Feign的日志级别 打印内容
NONE(默认值) 不记录任何日志
BASIC 仅记录请求方法、URL、响应状态码以及执行时间(适用于线上)
HEADERS 在BASIC的基础上,记录请求和响应的header
FULL 记录请求和响应的header、body和元数据
1、Java代码方式
  • A、在@FeignClient注解上加入configuration属性
@FeignClient(value="https://www.it610.com/article/price-center",configuration = PriceCenterFeignConfiguration.class) public interface PriceCenterFeignClient {@GetMapping(value = "https://www.it610.com/prices/{id}") PriceInfo getPrice(@PathVariable("id") Integer id); }

  • B、新建PriceCenterFeignConfiguration类,用于配置Feign日志级别
/** * 通过java代码,细粒度的方式(指定user-center)配置Feign的日志级别 * * 这个类别加@Configuration注解了,否则必须挪到@ComponentScan能扫描到的包以外 * */public class PriceCenterFeignConfiguration{@Bean public Logger.Level level(){ //让Feign打印所有请求的细节 return Logger.Level.FULL; } }

  • C、添加application.yml配置
logging: level: #配置Feign的日志级别 com.yibo.contentcenter.configuration.PriceCenterFeignConfiguration: debug

即Feign的日志级别是建立在上面配置的debug基础之上的,如果上面改为info,那么则不会输出任何日志
2、配置属性方式
首先去掉@FeignClient注解上的configuration属性
#细粒度的配置Feign的日志级别,这种是通过配置文件的方式 feign: client: config: #想要调用的微服务的名称 price-center: loggerLevel: full

全局自定义Feign的日志级别配置 Java代码方式
  • 方式一、让父子上下文ComponentScan重叠(强烈不建议使用)
  • 方式二、@EnableFeignClients(defaultConfiguration = xxx.class)(唯一正确方式)
@SpringBootApplication @EnableFeignClients(defaultConfiguration = GlobalFeignConfiguration.class) public class ContentCenterApplication {public static void main(String[] args) {SpringApplication.run(ContentCenterApplication.class, args); } }public class GlobalFeignConfiguration{@Bean public Logger.Level level(){ //让Feign打印所有请求的细节 return Logger.Level.FULL; } }

配置属性方式
首先去掉@EnableFeignClients中的defaultConfiguration属性
feign: client: config: #全局配置 default: loggerLevel: full

Feign支持的配置项 代码方式支持的配置项
配置项 作用
Feign.Builder Feign的入口
Client Feign底层用什么去请求
Contract 契约,注解支持
Encocer 编码器,用于将对象转换成HTTP请求消息体
Decoder 解码器,将相应消息体转换成对象
Logger 日志管理
LoggerLevel 指定日志级别
Retryer 指定重试策略
ErrorDecoder 指定错误解码器
Request.Options 超时时间
Collection 拦截器
SetterFactory 用于设置Hystrix的配置属性,Feign整合Hystrix才会用
属性方式支持的配置项
feign.client.config: : connectTimeout:5000#连接超时时间 readTimeout:5000#读取超时时间 loggerLevel:full#日志级别 errorDecoder:com.example.SimpleErrorDecoder#错误解码器 retryer:com.example.SimpleRetryer#重试策略 requestInterceptors: - com.example.FooRequestInterceptor#拦截器 # 是否对404错误码解码 # 处理逻辑详见feign.SynchronousMethodHandler#executeAndDecode decode404:false encoder:com.example.SimpleEncoder# 编码器 decoder:com.example.SimpleDecoder# 解码器 contract:com.example.SimpleContract# 契约

Feign配置代码方式 VS 属性方式
Feign配置优先级:全局代码<全局属性<细粒度代码<细粒度属性

Spring|Spring Cloud Feign组件
文章图片
image.png
  • 尽量使用属性配置,属性方式实现不了的情况下在考虑使用代码方式配置
  • 在同一微服务内尽量保持单一性,比如统一使用属性配置,不要两种方式混用,增加定位代码的复杂性
Feign的继承特性 Spring Cloud中Feign的继承特性:https://blog.csdn.net/u012702547/article/details/78261306
创建一个基础的Maven工程,定义Controller接口写好SpringMvc注解,由服务提供方和服务消费方通过引入基础Maven工程,分别继承Controller接口,Feign继承特性方式用起来确实很方面,但是也带来一个问题,就是服务提供者和服务消费者的耦合度太高,此时如果服务提供者修改了一个接口的定义,服务消费者可能也得跟着变化,进而带来很多未知的工作量,因此小伙伴们在使用继承特性的时候,要慎重考虑。
Feign脱离Ribbon的使用
@FeignClient(name="baidu",url= "http://www.baidu.com") public interface BaiduFeignClient {@GetMapping("") public string index(); }

RestTemplate VS Feign 尽量使用Feign,尽量杜绝使用RestTemplate
角度 RestTemplate Feign
可读性、可维护性 一般 极佳
开发体验 欠佳 极佳
性能 很好 中等(RestTemplate性能的50%左右)
灵活性 极佳 中等(内置功能可满足绝大多数需求)
Open Feign数据压缩功能
  • Spring Cloud Feign支持对请求和响应进行数据压缩(默认采用 gzip 压缩),以此来提高通信效率。
  • 如果在服务间单次传输的数据超过1K字节,强烈推荐开启数据压缩功能。
feign: compression: request: enabled: true# 开启请求的数据压缩 mime-types: text/xml,application/xml,application/json # 配置压缩支持的MIME TYPE min-request-size: 1024# 配置压缩数据大小的下限,当传输的数据类型大于 1024 时,才会进行压缩 response: enabled: true # 配置响应GZIP压缩

Feign的性能优化 1、为Feign配置连接池,性能提升15% Feign通过jdk中的HttpURLConnection向下游服务发起http请求(源码详见feign.Client)
public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = this.convertAndSend(request, options); return this.convertResponse(connection, request); }HttpURLConnection convertAndSend(Request request, Options options) throws IOException { HttpURLConnection connection = (HttpURLConnection)(new URL(request.url())).openConnection(); if (connection instanceof HttpsURLConnection) { HttpsURLConnection sslCon = (HttpsURLConnection)connection; if (this.sslContextFactory != null) { sslCon.setSSLSocketFactory(this.sslContextFactory); }if (this.hostnameVerifier != null) { sslCon.setHostnameVerifier(this.hostnameVerifier); } } ...... }

得出结论:缺乏连接池的支持,在达到一定流量的后服务肯定会出问题,可以用httpclient和okhttp替换掉jdk原生的HttpURLConnection,httpclient和okhttp都是支持连接池的
httpclient替换掉Feign原生的HttpURLConnection
  • 1、引入maven依赖
io.github.openfeign feign-httpclient

  • 2、添加配置
feign: httpclient: #让feign使用apache httpclient做请求,而不是默认的HttpURLConnection enabled: true #feign的最大连接数 max-connections: 200 #feign单个路径的最大连接数127.0.0.1:9876 max-connections-per-route: 50

okhttp替换掉Feign原生的HttpURLConnection
  • 1、引入maven依赖
io.github.openfeign feign-okhttp 10.1.0

  • 2、添加配置
feign: client: config: default: # 服务名,填写 default 为所有服务,或者指定某服务 connectTimeout: 10000 # 连接超时,10秒 readTimeout: 20000 # 读取超时,20秒 httpclient: enabled: false # 关闭 ApacheHttpClient max-connections: 200 # 连接池连接最大连接数 max-connections-per-route: 50 # feign单个路径的最大连接数127.0.0.1:9876 time-to-live: 600 # 连接最大闲置时间,单位为秒,600秒==10分钟(缺省值为 900秒==15分钟) okhttp: enabled: true # 开启 okhttp

  • 3、添加java代码配置
@Configuration @ConditionalOnClass(Feign.class) @AutoConfigureBefore(FeignAutoConfiguration.class) public class FeignOkHttpConfig {@Autowired OkHttpLoggingInterceptor okHttpLoggingInterceptor; @Bean public okhttp3.OkHttpClient okHttpClient(){ return new okhttp3.OkHttpClient.Builder() //读取超时时间 .readTimeout(60, TimeUnit.SECONDS) //连接超时时间 .connectTimeout(60, TimeUnit.SECONDS) //写超时时间 .writeTimeout(120, TimeUnit.SECONDS) //设置连接池 .connectionPool(new ConnectionPool()) // .addInterceptor(); .build(); } }

2、为Feign设置合理的日志级别
  • Feign默认是不打印任何日志的,这个日志级别的性能是最好的,但是生产环境如果需要了解请求的具体细节,那么建议将Feign的日志级别设置为BASIC。
Feign传递Token 利用@RequestHeader,强烈不建议使用这种方式 利用RequestInterceptor实现Token传递,推荐使用 1、新建TokenFeignClientInterceptor实现RequestInterceptor重写apply()
/** * 发送FeignClient设置Header信息 * * 微服务之间通过Feign调用,通过拦截器在feign请求之前,把当前服务的token添加到目标服务的请求头里 */@Component public class TokenFeignClientInterceptor implements RequestInterceptor {/** * token放在请求头 * @param requestTemplate */ @Override public void apply(RequestTemplate requestTemplate) { //1、从header里面获取token RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); if (requestAttributes != null) { HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); String token = request.getHeader("Authorization"); //2、将token传递 if(!StringUtils.isEmpty(token)){ requestTemplate.header("Authorization",token); } } } }

2、配置TokenFeignClientInterceptor
  • A、利用@FeignClient的属性configuration进行配置
@FeignClient(value="https://www.it610.com/article/priceServer",configuration = TokenFeignClientInterceptor.class) public interface PriceFeignClient {@GetMapping(value = "https://www.it610.com/prices/{id}") PriceInfo getPrice(@PathVariable("id") Integer id); }

  • B、使用application.yml进行全局属性配置
feign: client: config: default: requestInterceptors: - com.yibo.orderapi.feignclient.TokenFeignClientInterceptor

RestTemplate传递Token 1、exchange()
/** * 此方法为演示restTemplate传递token * @param id * @param request * @return */ @GetMapping("/tokenRelay/{id}") public ResponseEntity tokenRelay(@PathVariable("id") Integer id, HttpServletRequest request){ String token = request.getHeader("Authorization"); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Authorization",token); return restTemplate .exchange("http://user-center/users/{id}", HttpMethod.GET, new HttpEntity<>(httpHeaders), UserDTO.class, id); }

2、ClientHttpRequestInterceptor
/** * @Description: 通过此拦截器给RestTemplate请求添加Header传递Token */ public class TestRestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {@Override public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { //1、从header里面获取token RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); String token = request.getHeader("Authorization"); HttpHeaders headers = httpRequest.getHeaders(); headers.add("Authorization",token); //保证RestTemplate请求继续执行 return clientHttpRequestExecution.execute(httpRequest,bytes); } }@Bean @LoadBalanced @SentinelRestTemplate //RestTemplate整合Sentinel public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(); //通过给RestTemplate添加拦截器,达到通过RestTemplate传递Header restTemplate.setInterceptors(Collections.singletonList(new TestRestTemplateTokenRelayInterceptor())); return restTemplate; }

3、使用OAuth2RestTemplate
//此参数SpringBoot已经声明好了只需要注入即可用 @Autowired private OAuth2ProtectedResourceDetails resource; //此参数SpringBoot已经声明好了只需要注入即可用 @Autowired private OAuth2ClientContext context; //OAuth2RestTemplate在发送请求的时候,在Http请求头里面会自动放入收到的token @Bean @LoadBalanced public OAuth2RestTemplate oAuth2RestTemplate(){return new OAuth2RestTemplate(resource,context); }

RestTemplate整合HttpClient 引入maven依赖:
org.apache.httpcomponents httpclient 4.5.12

SpringBoot框架RestTemplate+Httpclient的代码配置:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class HttpClientRestConfig { @Bean public ClientHttpRequestFactory clientHttpRequestFactory() { HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); clientHttpRequestFactory.setHttpClient(HttpsClientPoolThread.builder().createSSLClientDefault()); //这里是使用了自定义的一个HttpsClientPoolThread线程池单例 以后有机会会单独写文章展示其配置内容, 大家可以先使用默认的HttpClients.createDefault()进行配置,或自定义线程池; clientHttpRequestFactory.setConnectTimeout(10000); clientHttpRequestFactory.setReadTimeout(10000); clientHttpRequestFactory.setConnectionRequestTimeout(200); return clientHttpRequestFactory; }@Bean public RestTemplate restTemplate() { return new RestTemplate(clientHttpRequestFactory()); } }

在这里 要说明下:
  • post put patch等请求 参数必须使用MultiValueMap进行接收和传递,否则 参数会为空!
  • get请求,如果需要使用Map传递参数,那么该Map一定不能是MultiValueMap! 否则, 传递的参数会附带上’[]’!
Spring框架的RestTemplate+Httpclient的代码配置:
@Configuration public class RestTemplateConfig { private Logger log = Logger.getLogger(this.getClass()); @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(clientHttpRequestFactory()); restTemplate.setErrorHandler(new DefaultResponseErrorHandler()); return restTemplate; } @Bean public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() { try { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() { public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { return true; } }).build(); httpClientBuilder.setSSLContext(sslContext); HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE; SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); Registry socketFactoryRegistry = RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslConnectionSocketFactory).build(); // 注册http和https请求 PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); // 开始设置连接池 poolingHttpClientConnectionManager.setMaxTotal(200); // 最大连接数200 poolingHttpClientConnectionManager.setDefaultMaxPerRoute(20); // 同路由并发数20 httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager); httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true)); // 重试次数 HttpClient httpClient = httpClientBuilder.build(); HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient连接配置 clientHttpRequestFactory.setConnectTimeout(20000); // 连接超时 clientHttpRequestFactory.setReadTimeout(20000); // 数据读取超时时间 clientHttpRequestFactory.setConnectionRequestTimeout(200); // 连接不够用的等待时间 return clientHttpRequestFactory; } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { log.error(e.getMessage()); } return null; } }

拓展:
OkHttp设计原理 深入理解Feign之源码解析 Feign常见问题总结 如何使用Feign构造多参数的请求 参考:
spring cloud feign使用okhttp3 https://www.cnblogs.com/zhangbing0615/articles/9238311.html

    推荐阅读