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);
}
}
}
转载
作者:景宗会
链接:https://www.jianshu.com/p/92d0537de737
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。