序言
在前面的例子中,我们使用了 Ribbon 的负载均衡功能,大大优化了远程调用时的代码:1
2String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
但是,我们以后可能需要编写类似的重复代码,格式基本相同,无非参数不一样,有没有更优雅的方式来对这些代码再次优化呢?
而且,我们使用了 Hystrix 的熔断功能,但将熔断的方法直接嵌套在业务代码中间,会不会显得有些杂乱?这一点又能不能优化呢?
当然可以,Feign 对 Ribbon 和 Hystrix 都进行了封装,上面 2 个都是小 case 。
Feign 基于 Netflix Feign 实现,它整合了 Ribbon 与 Hystrix , 除了提供这两者的强大功能之外,它还提供了一种声明式的 Web 服务客户端定义方式。
快速入门
下面我们对consumer
服务调用者模块用 Feign 进行改造。
① 添加依赖
首先,我们在consumer
模块的pom.xml
文件加入下面的依赖:1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
当加入上面依赖后,可以去除 Ribbon 相关依赖,因为openfeign
已包含它们。
② 添加注解
Ribbon 负载均衡的支持
我们在启动类上添加@EnableFeignClients
注解,并删除一些代码(注释部分):1
2
3
4
5
6
7
8
9
10
11
12
public class ConsumerApplication {
// @Bean
// @LoadBalanced
// public RestTemplate restTemplate() {
// return new RestTemplate();
// }
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
删除了RestTemplate
这个代表 HTTP 客户端工具的Bean
,那么 Ribbon 的负载均衡到哪去了?后续又如何远程调用服务呢?
第一个问题答案: Feign 内部直接实现了 Ribbon 的负载均衡。
第二个问题答案:远程调用服务可以使用Feign
提供的 HTTP 客户端工具。具体而言,可以新建一个接口,在接口上@FeignClient
注解告诉客户端要调用的服务名,并且在接口中编写要访问服务的具体方法:
1 | "user-service" ) (value = |
之后直接在controller
层注入该 HTTP 客户端工具UserClient
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"consumer") (
"defaultFallback") (defaultFallback =
public class ConsumerController {
private UserClient userClient;
"{id}") (
(commandProperties = {
"execution.isolation.thread.timeoutInMilliseconds", value = "2000") (name =
})
public User queryById(@PathVariable("id") Integer id) {
return userClient.queryById(id);
}
public String defaultFallback() {
return "不好意思,服务器太拥挤了,请稍后再试";
}
}
Hystrix 的支持(建议使用 Sentinel 替代)
前面说过,将熔断的方法直接嵌套在业务代码中间,代码会显得有些杂乱。
而 Feign ,可以将 Hystrix 的熔断方法抽离出来,使得代码更为优雅。
那我们来修改代码吧:1
2
3
4
5"user-service",fallback = UserClientFallback.class ) (value =
public interface UserClient {
"user/{id}") (
User queryById(@PathVariable("id") Integer id);
}
下面是熔断方法直接指定的特定类UserClientFallback
的详细代码:1
2
3
4
5
6
7
8
9
public class UserClientFallback implements UserClient {
public User queryById(Integer id) {
User user = new User();
user.setName(null);
return user;
}
}
③ 修改配置文件
我们可以在application.yml
文件中修改对 Ribbon 和 Hystrix 的配置,和以前有所不同:1
2
3
4
5
6
7
8
9
10
11
12
13feign:
hystrix:
enabled: true
ribbon:
CoonectionTimeout: 500
ReadTimeout: 2000
hystrix:
commond:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
Feign 默认是不开启 Hystrix 的,所以我们需要将其开启。
之后就可以进行测试了(测试时别忘了将模拟超时的线程睡眠代码去除),http://127.0.0.1:8081/user/1
测试结果如下:
1 | {"id":1,"username":"lucy","password":"123","name":"章总","telephone":0} |
负载均衡
Feign 封装了 Ribbon自然也就集成了负载均衡的功能,默认采用轮询策略。
那么如何修改负载均衡策略呢?
全局配置
在启动类或配置类中注入负载均衡策略对象。所有服务请求均使用该策略。1
2
3
4
public RandomRule randomRule(){
return new RandomRule():
}
局部配置
1 | service- provider: |
自定义配置
Feign 允许通过自定义配置来覆盖默认配置,可以修改的配置项如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level |
修改日志级别 | 包含四种不同的级别:NONE、 BASIC、HEADERS、FULL |
feign.codec.Decoder |
响应结果的解析器 | HTTP 远程调用的结果做解析,例如解析 Json 字符串为 Java对象 |
feign.codec.Encoder |
请求参数编码 | 将请求参数编码,便于通过 HTTP 请求发送 |
feign.Contract |
支持的注解格式 | 默认是 SpringMVC 的注解 |
feign.Retryer |
失败重试机制 | 默认无,但会使用 Ribbon 的重试 |
日志配置
如何查看 Feign 调用时的日志?
这自然可以通过相关配置实现,不过笔者建议关闭 Feign 日志记录,因为一般接口调用时会记录日志入库,那我还多记录一次干啥?
在 Feign 中,存在两种方式配置日志:
- ① 文件配置
- ② Java 代码配置
① 文件配置
全局配置
1 | feign: |
特定服务配置
1 | feign: |
② Java 代码配置类
声明配置类
此方式需要先声明一个配置类:1
2
3
4
5
6
7
public class FeignClientConfiguration {
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;
}
}
使用
全局配置
若为全局配置,则把配置类放到启动类的 @EnableFeignClients
注解中..
1 | (defaultConfiguration = FeignClientConfiguration.class) |
局部配置
若为局部配置,则把配置类放到@FeignClient
这个注解中:
1 | "userservice",configuration = FeignClientConfiguration.class) (value = |
最佳实践
集成优化
如何在项目中集成 Feign,企业中通常使用两种方式:
- 继承
- 抽离(建议)
继承
给消费者的FeignClient
和提供者的controller
定义统一的父接口作为标准。
1 | public interface UserAPI{ |
1 | "userservice") (value = |
1 |
|
此方式耦合性过高,且对 Spring MVC 相关注解不兼容,不建议使用。
抽离(建议)
将FeignClient
抽离为独立模块,并且把接口有关的POJO
、默认的Feign
配置都放到这个模块中,提供给所有消费者使用。
此方式实现步骤如下:
- ① 首先创建子模块,命名为
feign-api
, 然后引入feign
的starter
依赖 - ② 将
consumer-service
中编写的UserClient
及相关POJO
类复制到feign-api
模块中 - ③ 在
consumer-service
中引入feign-api
的依赖 - ④ 在
consumer-service
新增相关配置类
注意
在consumer-service
中,当定义的FeignClient
不在SpringBootApplication
的扫描包范围时,这些FeignClient
无法使用。
存在两种方式解决该问题,在启动类@EnableFeignClients
注解上:
指定
FeignClient
所在包(建议)1
"cn.xx.feign.clients") (basePackages =
指定具体的
FeignClient
类1
(clients = {UserClient.class})
性能优化
Feign 底层使用的 HTTP 连接工具为URLConnection
,由于其不支持连接池,性能较差,为了提升性能,我们可以更换 Feign 底层的 HTTP 连接工具为Apache HttpClient
,具体集成步骤如下:
- 引入依赖
- 配置开启
引入依赖
1 | <dependency> |
配置开启
1 | feign: |
数据传递
由于 Feign 是服务间接口调用,由于接口基本上都会做权限检验,所以 Feign 调用时需要注意传递相关参数。
实施起来的具体方案,是通过 Feign 的拦截器进行设置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class FeignRequestInterceptor implements RequestInterceptor {
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
if (StringUtils.isNotNull(request)) {
Map<String, String> headers = new LinkedCaseInsensitiveMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
headers.put(key, value);
}
}
// 传递 Authorization Header
String authentication = headers.get(HttpHeaders.AUTHORIZATION);
if (StringUtils.isNotEmpty(authentication)) {
requestTemplate.header(HttpHeaders.AUTHORIZATION, authentication);
}
// 配置客户端 IP
requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr(request));
}
}
}
参考
文章信息
时间 | 说明 |
---|---|
2019-12-22 | 初版 |
2022-11-22 | 重构 |