1 Feign
1.1 定义
1.1.1 简介
Feign
是一个声明式的Web Service
客户端,通过声明RESTful
请求客户端
Spring Cloud
集成了Ribbon
和Eureka
,可在使用Feign
时提供负载均衡的http
客户端
微服务直接调用使用RestTemplate
进行远程调用,非常方便,那么有了RestTemplate
为什么还要有Feign
,因为RestTemplate
有一个致命的问题:硬编码
。
点击了解Spring之RestTemplate
在 RestTemplate
调用中,我们每个调用远程接口的方法,都将远程接口对应的 ip、端口,或 service-id 硬编码到了 URL 中,如果远程接口的 ip、端口、service-id 有修改的话,需要将所有的调用都修改一遍,这样难免会出现漏改、错改等问题,且代码不便于维护。为了解决这个问题,Netflix
推出了 Feign
来统一管理远程调用
1.1.2 属性介绍
@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
类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码 -
path
: 定义当前FeignClient
的统一前缀 -
primary
: 默认值true
,Feign
会自动生成FeignClient
,也就是接口的jdk代理实现
注意
:name/value
和url
同时存在生效问题
-
name/value
属性:这两个的作用是一样的,指定的是调用服务的微服务名称 -
url
:指定调用服务的全路径,经常用于本地测试 - 如果同时指定
name
和url
属性: 则以url
属性为准,name
属性指定的值便当做客户端的名称
1.1.3 原理解析
Feign
调用步骤:
- 程序启动时,扫描所有的
@FeignClient
注解 - 当接口方法被调用时,通过
JDK
代理来生成RequestTemplate
模板 - 根据
RequestTemplate
模板生成Http
请求的Request
对象 -
Request
对象交给Client
去处理,其中Client
的网络请求框架可以是HttpURLConnection、HttpClient、OKHttp
- 最后
client
封装成LoadBaLanceClient
,结合ribbon
负载均衡地发起调用
1.2 Feign准备工作
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
1.3 使用Feign
1.3.1 简单使用@FeignClient
@FeignClient(value = "CART",fallback=Hysitx.class,configuration = FeignConfiguration.class)
public interface CartFeignClient {
@PostMapping("/cart/{productId}")
Long addCart(@PathVariable("productId")Long productId);
}
编写熔断类,发生错误时回调
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 其他操作
1.5.1 使用OKhttp
Feign
底层默认是使用jdk
中的HttpURLConnection
发送HTTP
请求,feign
也提供了OKhttp
来发送请求,具体配置如下
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
okhttp:
enabled: true
hystrix:
enabled: true
1.5.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);
}
}
}
网友评论