Spring Cloud Feign组件
概述
Feign 是Netflix开源的一个声明式的Http 客户端,它的目的就是让Web Service基于Http的远程调用变得更加简单。
Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign 默认集成了 Ribbon,Nacos 也很好的兼容了 Feign,默认实现了负载均衡的效果。
- Feign 采用的是基于接口的注解
- Feign 整合了 ribbon
快速入门
1、引入maven依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--整合Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2、通过@EnableFeignClients注解开启Feign功能
@SpringBootApplication
@EnableFeignClients
public class OrderApiApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApiApplication.class, args);
}
}
3、创建Feign接口,通过 @FeignClient("服务名") 注解来指定调用哪个服务
@FeignClient(value="price-center")
public interface PriceCenterFeignClient {
@GetMapping(value = "/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="price-center",configuration = PriceCenterFeignConfiguration.class)
public interface PriceCenterFeignClient {
@GetMapping(value = "/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<RequestInterceptor> | 拦截器 |
SetterFactory | 用于设置Hystrix的配置属性,Feign整合Hystrix才会用 |
属性方式支持的配置项
feign.client.config:
<feignName>:
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配置优先级:全局代码<全局属性<细粒度代码<细粒度属性
- 尽量使用属性配置,属性方式实现不了的情况下在考虑使用代码方式配置
- 在同一微服务内尽量保持单一性,比如统一使用属性配置,不要两种方式混用,增加定位代码的复杂性
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();
}
Feign调用https接口的话需要绕过SSL验证
Feign- 绕过SSL验证的方案
方案一——改写LoadBalancerFeignClient
import feign.Client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
/**
* @Author: huangyibo
* @Date: 2022/8/3 19:36
* @Description: feign client配置
*/
public class FeignConfiguration {
@Bean
public CachingSpringLoadBalancerFactory cachingFactory(SpringClientFactory clientFactory) {
return new CachingSpringLoadBalancerFactory(clientFactory);
}
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext ctx = SSLContext.getInstance("TLSv1.2");
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);
return new LoadBalancerFeignClient(new Client.Default(ctx.getSocketFactory(),
(hostname, session) -> true),
cachingFactory, clientFactory);
}
}
@FeignClient(name = "OrderFeign", url= "https://yibo.com/order-center/openapi", configuration = FeignConfiguration.class)
public interface OrderFeign {
@PostMapping("/order")
ResultBody<List<HouseDO>> queryHouseList(OrderQuery query);
}
方案二——改写RestTemplate
@Bean
public RestTemplate getRestTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext,
new String[]{"TLSv1.2"},
null,
NoopHostnameVerifier.INSTANCE);
CloseableHttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(csf)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
return new RestTemplate(requestFactory);
}
@FeignClient(name = "OrderFeign", url= "https://yibo.com/order-center/openapi")
public interface OrderFeign {
@PostMapping("/order")
ResultBody<List<HouseDO>> queryHouseList(OrderQuery query);
}
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依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 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依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>10.1.0</version>
</dependency>
- 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="priceServer",configuration = TokenFeignClientInterceptor.class)
public interface PriceFeignClient {
@GetMapping(value = "/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<UserDTO> 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依赖:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
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<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>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构造多参数的请求
参考: