SpringCloud之Feign,OpenFeign和Feign拦截器讲解
1 Feign
1.1 定义
1.1.1 简介
Feign
是一个声明式的Web Service
客户端,通过声明RESTful
请求客户端
Spring Cloud
集成了Ribbon
和Eureka
,可在使用Feign
时提供负载均衡的http
客户端
Java
当中常见的Http
客户端有很多,除了Feign
,类似的还有Apache
的 HttpClient
以及OKHttp3
,还有SpringBoot
自带的RestTemplate
这些都是Java当中常用的HTTP 请求工具
微服务直接调用使用RestTemplate
进行远程调用,非常方便,那么有了RestTemplate
为什么还要有Feign
,因为RestTemplate
有一个致命的问题:硬编码
。
点击了解Spring之RestTemplate
在 RestTemplate
调用中,我们每个调用远程接口的方法,都将远程接口对应的 ip、端口,或 service-id 硬编码到了 URL 中,如果远程接口的 ip、端口、service-id 有修改的话,需要将所有的调用都修改一遍,这样难免会出现漏改、错改等问题,且代码不便于维护。为了解决这个问题,Netflix
推出了 Feign
来统一管理远程调用
1.1.2 OpenFeign和Feign的区别
Feign:
Feign
是Spring Cloud
组件中的一个轻量级RESTful
的HTTP
服务客户端,Feign
内置了Ribbon
,用来做客户端负载均衡,去调用服务注册中心的服务。Feign
的使用方式是:使用Feign
的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
OpenFeign:
OpenFeign
是Spring Cloud
在Feign
的基础上支持了SpringMVC
的注解,如@RequesMapping
等等。OpenFeign
的@FeignClient
可以解析SpringMVC
的@RequestMapping
注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Feign
是在2019就已经不再更新了,通过maven
网站就可以看出来,随之取代的是OpenFeign
,从名字上就可以知道,它是Feign
的升级版
1.1.3 属性介绍
@FeignClient
标签的常用属性如下:
name
:指定FeignClient
的名称,如果项目使用了Ribbon
,name
属性会作为微服务的名称,用于服务发现value
: 调用服务名称,和name
属性相同url
:url
一般用于调试,可以手动指定@FeignClient
调用的地址decode404
: 当发生http 404
错误时,如果该字段位true
,会调用decoder
进行解码,否则抛出FeignException
configuration
:Feign
配置类,可以自定义Feign
的Encoder、Decoder、LogLevel、Contract
fallback
: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback
指定的类必须实现@FeignClient
标记的接口
熔断机制,调用失败时,走的一些回退方法,可以用来抛出异常或给出默认返回数据。底层依赖hystrix
,启动类要加上@EnableHystrix
fallbackFactory
: 工厂类,用于生成fallback
类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
另外,fallbackFactory
可以捕获异常信息并返回默认降级结果,可以打印堆栈信息,但是fallback
不能打印堆栈信息,不利于问题排查path
: 定义当前FeignClient
的统一前缀primary
: 默认值true
,Feign
会自动生成FeignClient
,也就是接口的jdk代理实现
注意
:name/value
和url
同时存在生效问题
name/value
属性:这两个的作用是一样的,指定的是调用服务的微服务名称url
:指定调用服务的全路径,经常用于本地测试- 如果同时指定
name
和url
属性,则以url
属性为准,name
属性指定的值便当做客户端的名称
1.1.4 原理解析
Feign
调用步骤:
- 程序启动时,扫描所有的
@FeignClient
注解 - 当接口方法被调用时,通过
JDK
代理来生成RequestTemplate
模板 - 根据
RequestTemplate
模板生成Http
请求的Request
对象 Request
对象交给Client
去处理,其中Client
的网络请求框架可以是HttpURLConnection、HttpClient、OKHttp
- 最后
client
封装成LoadBaLanceClient
,结合ribbon
负载均衡地发起调用
1.1.5 负载均衡策略
主要有七种;
RoundRobinRule
:轮询策略,按照服务顺序依次循环调用WeightedResponseTimeRule
:权重比策略,优先选择权重比高的服务,也就是服务响应时间比较短的,响应时间越长权重比越低RandomRule
:随机策略,服务提供者列表随机选择一个服务BestAvailableRule
:最小连接数策略,获取服务列表中连接数最小的服务实例RetryRule
:重试策略,重试获取已经失效的服务,指定时间没有获取到返回NULLAvailabilityFilteringRule
:可用性敏感策略,过滤非健康服务实例,选择lianjiZoneAvoidanceRule
:区域敏感策略
1.1.6 饥饿加载
Ribbon-eager-load
(饥饿加载)模式
Ribbon
对于负载 Client
是在服务启动后,发生调用的时候才会去创建 Client
,所以在第一次发生 http
请求调用的时候,不光要算上 http
的请求时间,还要算上 Client 创建时间
,所以第一次调用的时候才会很慢
开启Ribbon
饥饿加载
ribbon:
nacos:
enabled: true # 开启naocos轮询
eager-load:
enabled: true # 开启Ribbon的饥饿加载模式(防止第一次请求超时的问题)
clients: Lxlxxx-system2 # 指定需要开启的服务(需要开启Ribbon的饥饿加载模式)
ReadTimeout: 10000
ConnectTimeout: 10000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
OkToRetryOnAllOperations: false
其实这种饥饿加载模式,类似于客户端负载预热
的一个操作,项目启动的时候进行加载,防止服务之间调用可以因为数据量、业务逻辑处理复杂性导致接口超时
1.2 OpenFeign准备工作
1.2.1 引入依赖
在Spring Cloud
项目中引入Feign
依赖,但是因为feign
底层是使用了ribbon
作为负载均衡的客户端,而ribbon
的负载均衡也是依赖于eureka
获得各个服务的地址,所以要引入eureka-client
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
随便写的版本号
<version>2.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
随便写的版本号
<version>2.0.2.RELEASE</version>
</dependency>
1.2.2 启动类和yml文件
需要在启动类上添加注解@EnableFeignClients
以及@EnableDiscoveryClient
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
server:
port: 8082
#配置eureka
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
status-page-url-path: /info
health-check-url-path: /health
#服务名称
spring:
application:
name: product
profiles:
active: ${boot.profile:dev}
#feign的配置,连接超时及读取超时配置
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
注意
:如果在@EnableFeignClients
注解中不指定basePackages
属性,Feign
会扫描当前包
及其子包
下的所有接口,如果接口不在这些包中,则会报错。因此,为了避免这种情况,需要在@EnableFeignClients
注解中指定basePackages
属性,以明确指定需要扫描的包
1.3 使用OpenFeign
1.3.1 简单使用@FeignClient
@FeignClient(value = "CART",fallback=Hysitx.class,configuration = FeignConfiguration.class)
public interface CartFeignClient {
//@PostMapping是调用 目标服务的controller的方法,和对应controller路径保持一致
@PostMapping("/cart/{productId}")
Long addCart(@PathVariable("productId")Long productId);
@GetMapping(value = "/payment/selectPaymentList")
CommonResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);
@GetMapping(value = "/payment/selectPaymentListByQuery")
CommonResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);
@PostMapping(value = "/payment/create", consumes = "application/json")
CommonResult<Payment> create(@RequestBody Payment payment);
@GetMapping("/payment/getPaymentById/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") String id);
}
@SpringQueryMap
:
spring cloud
项目使用feign
的时候都会发现一个问题,就是get
方式无法解析对象参数。其实feign
是支持对象传递的,但是得是Map
形式,而且不能为空,与spring
在机制上不兼容,因此无法使用。spring cloud
在2.1.x
版本中提供了@SpringQueryMap
注解,可以传递对象参数,框架自动解析。
编写熔断类,发生错误时回调
import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class Hysitx implements IRemoteCallService{
@Override
public List<String> test(String[] names) {
System.out.println("接口调用失败");
return null;
}
}
引入FeignAutoConfiguration
配置
@Import(FeignAutoConfiguration.class)
@Configuration
public class FeignConfiguration{
...
}
上面是最简单的feign client的使用,声明完为feign client后,其他spring管理的类,如service就可以直接注入使用了,例如:
//这里直接注入feign client
@Autowired
private CartFeignClient cartFeignClient;
@PostMapping("/toCart/{productId}")
public ResponseEntity addCart(@PathVariable("productId") Long productId){
Long result = cartFeignClient.addCart(productId);
return ResponseEntity.ok(result);
}
1.3.2 @RequestLine
Feign
为什么用的是@RequestLine
- 这和
open-feign
的Contract
设计有关系,Contract
是一个注解解析接口,它决定了接口可以使用什么注解转换到http
请求。open-feign
在使用@FeignClient
的情况下,使用的是SpringMvcContract
,它使得被@FeignClient
修饰的接口,可以使用@GetMapping,@PostMapping
等Spring Mvc注解。
如果我们要使用@RequestLine
,则需要替换open-Feign
的MVC
解析器
// 在feign上写上配置
@FeignClient(name = "test-center", configuration = TestFeignConfig .class)
@Configuration// 配置类
public class TestFeignConfig {
@Bean
public Contract feignContract() {
// 配置feign的注释解析器为feign默认解析器而不是mvc解析器
return new feign.Contract.Default();
}
}
如果我们不单独配置,则会使用FeignClientsConfiguration
中默认配置的SpringMvcContract
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
@RequestLine
与其它请求不同,只需要简单写请求方式和路径就能达到请求其它服务的目的
@FeignClient(value = "feign-server",configuration = FeignConfig.class) //需要一个配置文件
public interface TestService {
@RequestLine("POST /feign/test") //对应请求方式和路径
String feign(@RequestBody UserDO userDO);
}
配置文件
@EnableFeignClients
@SpringBootConfiguration
public class FeignConfig {
@Bean
public Contract contract(){
return new feign.Contract.Default();
}
}
@SpringBootConfiguration
- 标注这个类是一个配置类;
- 它只是
@Configuration
注解的派生注解; - 它与
@Configuration
注解的功能一致; - 只不过
@SpringBootConfiguration
是springboot
的注解,而@Configuration
是spring
的注解
1.4 OpenFeign添加header信息
在微服务间使用Feign
进行远程调用时需要在header
中添加信息,那么 springcloud open feign
如何设置 header
呢?有5种方式可以设置请求头信息:
- 在
@RequestMapping
注解里添加headers
属性 - 在方法参数前面添加
@RequestHeader
注解 - 在方法或者类上添加
@Headers
的注解 - 在方法参数前面添加
@HeaderMap
注解 - 实现
RequestInterceptor
接口
1.4.1 在@RequestMapping注解里添加headers属性
在application.yml
中配置
app.secret: appSecretVal
@PostMapping(value = "/book/api", headers = {"Content-Type=application/json;charset=UTF-8", "App-Secret=${app.secret}"})
void saveBook(@RequestBody BookDto condition);
1.4.2 在方法参数前面添加@RequestHeader注解
设置单个header
属性
@GetMapping(value = "/getStuDetail")
public StudentVo getStudentDetail(@RequestBody StudentDto condition, @RequestHeader("Authorization") String token);
设置多个header属性
@PostMapping(value = "/card")
public CardVo createCard(@RequestBody CardDto condition, @RequestHeader MultiValueMap<String, String> headers);
1.4.3 在方法或者类上添加@Headers的注解
使用feign
自带契约
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}
FeignClient
使用@RequestLine
注解, 而未配置feign
自带契约Contract
时, @Headers
不会起作用, 而且启动项目会报错:
Method xxx not annotated with HTTP method type (ex. GET, POST)
@RequestLine is a core Feign annotation, but you are using the Spring Cloud @FeignClientwhich uses Spring MVC annotations.
配置@Headers
注解
@FeignClient(url = "${user.api.url}", name = "user", configuration = FooConfiguration.class)
public interface UserFeignClient {
@RequestLine("GET /simple/{id}")
@Headers({"Content-Type: application/json;charset=UTF-8", "Authorization: {token}"})
public User findById(@Param("id") String id, @Param("token") String token);
}
使用@Param
可以动态配置Header属性
网上很多在说 @Headers
不起作用,其实@Headers
注解没有生效的原因是:官方的Contract
没有生效导致的
1.4.4 在方法参数前面添加@HeaderMap注解
使用feign
自带契约
配置@HeaderMap
注解
@FeignClient(url = "${user.api.url}", name = "user", configuration = FooConfiguration.class)
public interface UserFeignClient {
@RequestLine("GET /simple/{id}")
public User findById(@Param("id") String id, @HeaderMap HttpHeaders headers);
}
1.4.5 实现RequestInterceptor接口
值得注意的一点是:
如果FeignRequestInterceptor
注入到spring
容器的话就会全局生效, 就是说即使在没有指定configuration
属性的FeignClient
该配置也会生效
配置@Component
或@Service
或@Configuration
就可以将该配置注入spring
容器中, 即可实现全局配置, 从而该项目中的所有FeignClient
的feign
接口都可以使用该配置.
如果只想给指定FeignClient
的feign
接口使用该配置, 请勿将该类配置注入spring中.
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header(HttpHeaders.AUTHORIZATION, "tokenVal");
}
}
1.5 OpenFeign日志打印
Feign
提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign
中 Http
请求的细节。
说白了就是对Feign
接口的调用情况进行监控和输出
日志级别:
NONE
:默认的,不显示任何日志;BASIC
:仅记录请求方法、URL
、响应状态码及执行时间;HEADERS
:除了BASIC
中定义的信息之外,还有请求和响应的头信息;FULL
:除了HEADERS
中定义的信息之外,还有请求和响应的正文及元数据。
配置日志Bean:
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
YML
文件里需要开启日志的Feign客户端
logging:
level:
# feign日志以什么级别监控哪个接口
com.gzl.cn.service.PaymentFeignService: debug
后台日志查看:
1.6 手动创建 Feign 客户端
@FeignClient
无法支持同一service
具有多种不同配置的FeignClient
,因此,在必要时需要手动build FeignClient
。
@FeignClient(value = “CLOUD-PAYMENT-SERVICE”)
以这个为例,假如出现两个服务名称为CLOUD-PAYMENT-SERVICE
的FeignClient
,项目直接会启动报错,但是有时候我们服务之间调用的地方较多,不可能将所有调用都放到一个FeignClient
下,这时候就需要自定义来解决这个问题!
官网当中也明确提供了自定义FeignClient
,以下是在官网基础上对自定义FeignClient
的一个简单封装
首先创建FeignClientConfigurer
类,这个类相当于build FeignClient
的工具类
import feign.*;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.slf4j.Slf4jLogger;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;
@Import(FeignClientsConfiguration.class)
public class FeignClientConfigurer {
private Decoder decoder;
private Encoder encoder;
private Client client;
private Contract contract;
public FeignClientConfigurer(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.decoder = decoder;
this.encoder = encoder;
this.client = client;
this.contract = contract;
}
public RequestInterceptor getUserFeignClientInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
// 添加header
}
};
}
public <T> T buildAuthorizedUserFeignClient(Class<T> clazz, String serviceName) {
return getBasicBuilder().requestInterceptor(getUserFeignClientInterceptor())
//默认是Logger.NoOpLogger
.logger(new Slf4jLogger(clazz))
//默认是Logger.Level.NONE(一旦手动创建FeignClient,全局配置的logger就不管用了,需要在这指定)
.logLevel(Logger.Level.FULL)
.target(clazz, buildServiceUrl(serviceName));
}
private String buildServiceUrl(String serviceName) {
return "http://" + serviceName;
}
protected Feign.Builder getBasicBuilder() {
return Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract);
}
}
使用工具类的方法创建多个FeignClient
配置
import com.gzl.cn.service.FeignTest1Service;
import feign.Client;
import feign.Contract;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignClientConfiguration extends FeignClientConfigurer {
public FeignClientConfiguration(Decoder decoder, Encoder encoder, Client client, Contract contract) {
super(decoder, encoder, client, contract);
}
@Bean
public FeignTest1Service feignTest1Service() {
return super.buildAuthorizedUserFeignClient(FeignTest1Service.class, "CLOUD-PAYMENT-SERVICE");
}
// 假如多个FeignClient在这里定义即可
}
其中,super.buildAuthorizedUserFeignClient()
方法中,第一个参数为调用别的服务的接口类,第二个参数为被调用服务在注册中心的service-id。
public interface FeignTest1Service {
@GetMapping(value = "/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
使用的时候正常注入使用即可
@Resource
private FeignTest1Service feignTest1Service;
1.7 Feign配置
Feign
底层默认是使用jdk
中的HttpURLConnection
发送HTTP
请求(默认不支持线程池,若请求多了会响应超时),feign
也提供了OKhttp
来发送请求
feign.client.*
:支持按实例进行配置,feign.httpclient.*
:全局共享一套配置,包含线程池配置,但只影响HttpClient
和OkHttp
,不影响HttpURLConnection
所谓按实例进行配置,就是指每个FeignClient
实例都可以通过feign.client..*
来单独进行配置,注意首字母小写。而 feign.client.default.*
表示默认配置
1.7.1 使用OKhttp
具体配置如下
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
okhttp:
enabled: true
hystrix:
enabled: true
1.7.2 开启GZIP压缩
Spring Cloud Feign
支持对请求和响应进行GZIP
压缩,以提高通信效率。
application.yml
配置信息如下:
feign:
compression:
request: #请求
enabled: true #开启
mime-types: text/xml,application/xml,application/json #开启支持压缩的MIME TYPE
min-request-size: 2048 #配置压缩数据大小的下限
response: #响应
enabled: true #开启响应GZIP压缩
注意
:
由于开启GZIP
压缩之后,Feign
之间的调用数据通过二进制协议进行传输,返回值需要修改为ResponseEntity<byte[]>
才可以正常显示,否则会导致服务之间的调用乱码。
示例如下:
@PostMapping("/order/{productId}")
ResponseEntity<byte[]> addCart(@PathVariable("productId") Long productId);
2 Feign拦截器RequestInterceptor
2.1 定义
在使用feign
做服务间调用的时候,如何修改请求的头部或编码信息呢,可以通过实现RequestInterceptor
接口的apply
方法,feign
在发送请求之前都会调用该接口的apply
方法,所以我们也可以通过实现该接口来记录请求发出去的时间点
2.2 RequestInterceptor
public interface RequestInterceptor {
/**
* Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}
RequestInterceptor
接口定义了apply
方法,其参数为RequestTemplate
;它有一个抽象类为BaseRequestInterceptor
,还有几个实现类分别为BasicAuthRequestInterceptor、FeignAcceptGzipEncodingInterceptor、FeignContentGzipEncodingInterceptor
BasicAuthRequestInterceptor
: 实现了RequestInterceptor
接口,其apply
方法往RequestTemplate
添加名为Authorization
的header
BaseRequestInterceptor
: 定义了addHeader
方法,往requestTemplate
添加非重名的header
FeignAcceptGzipEncodingInterceptor
: 继承了BaseRequestInterceptor
,它的apply
方法往RequestTemplate
添加了名为Accept-Encoding
,值为gzip,deflate
的header
FeignContentGzipEncodingInterceptor
: 继承了BaseRequestInterceptor
,其apply
方法先判断是否需要compression
,即mimeType
是否符合要求以及content
大小是否超出阈值,需要compress
的话则添加名为Content-Encoding
,值为gzip,deflate
的header
2.3 配置RequestInterceptor
可以通过配置类和配置文件两种方式注册RequestInterceptor
- 通过配置类配置时,通过
FeignContext
获取RequestInterceptor bean
; - 通过配置文件注册时,通过
ApplicationContext
获取RequestInterceptor bean
通过配置类进行配置
设置配置类有两种方式
2.3.1 通过@EnableFeignClients的defaultConfiguration属性配置
@EnableFeignClients
注解的 defaultConfiguration
属性值为 @Configuration
注解的配置类,配置类内定义的 bean
会注册为所有 @FeignClient
的默认配置。
配置信息会保存为FeignClientSpecification
对象。
2.3.2 通过@FeignClient的configuration属性配置
@FeignClient
注解的 configuration
属性值为 @Configuration
注解的配置类,其定义的 bean
为每个FeignClient
的专有 bean
FeignContext
为每一个 @FeignClient
注解的接口提供独立的AnnotationConfigApplicationContext
,其中包含 FeignClient
的私有bean
和 EnableFeignClient
的默认 bean
。
2.3.3 通过配置文件配置
@Component
public class DpAuthFeignReqInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String jwtToken = request.getHeader(JWTTokenConstant.TOKEN_NAME);
//传递token 给下游
template.header(JWTTokenConstant.TOKEN_NAME, jwtToken);
}
}
}