Spring Cloud 声明式服务调用组件 Feign

序言

  在前面的例子中,我们使用了 Ribbon 的负载均衡功能,大大优化了远程调用时的代码:

1
2
String 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
@EnableFeignClients
@SpringCloudApplication
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
2
3
4
5
@FeignClient(value = "user-service" )
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") Integer id);
}

  之后直接在controller层注入该 HTTP 客户端工具UserClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Autowired
private UserClient userClient;

@GetMapping("{id}")
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
public User queryById(@PathVariable("id") Integer id) {
return userClient.queryById(id);
}
public String defaultFallback() {
return "不好意思,服务器太拥挤了,请稍后再试";
}
}

Hystrix 的支持(建议使用 Sentinel 替代)

  前面说过,将熔断的方法直接嵌套在业务代码中间,代码会显得有些杂乱。
  而 Feign ,可以将 Hystrix 的熔断方法抽离出来,使得代码更为优雅。
  那我们来修改代码吧:

1
2
3
4
5
@FeignClient(value = "user-service",fallback = UserClientFallback.class )
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") Integer id);
}

  下面是熔断方法直接指定的特定类UserClientFallback的详细代码:

1
2
3
4
5
6
7
8
9
@Component
public class UserClientFallback implements UserClient {
@Override
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
13
feign:
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
@Bean
public RandomRule randomRule(){
return new RandomRule():
}

局部配置

1
2
3
service- provider:
ribbon:
NFLoaddBalancerRuleC1assName: com.netflix.loadbalancer.RandomRule

自定义配置

  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
2
3
4
5
6
feign:
client:
config:
# default 代表全局配置,若写服务名称,则是针对某个微服务的配置
default:
logger-level: FULL

特定服务配置

1
2
3
4
5
6
feign:
client:
config:
# 服务名称,则针对用户服务
user-service:
logger-level: FULL

② Java 代码配置类

声明配置类

  此方式需要先声明一个配置类:

1
2
3
4
5
6
7
@Configuration
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC;
}
}

使用

全局配置

  若为全局配置,则把配置类放到启动类的 @EnableFeignClients注解中..

1
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
局部配置

  若为局部配置,则把配置类放到@FeignClient这个注解中:

1
@FeignClient(value ="userservice",configuration = FeignClientConfiguration.class)

最佳实践

集成优化

  如何在项目中集成 Feign,企业中通常使用两种方式:

  • 继承
  • 抽离(建议)

继承

  给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。

1
2
3
4
public interface UserAPI{
@GetMapping("/user/{id}")
User findById (@PathVariable("id") Long id);
}
1
2
@FeignClient(value = "userservice") 
public interface UserClient extends UserAPI{}
1
2
3
@RestController
public class UserController implements UserAPI{

  此方式耦合性过高,且对 Spring MVC 相关注解不兼容,不建议使用。

抽离(建议)

  将FeignClient抽离为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。

  此方式实现步骤如下:

  • ① 首先创建子模块,命名为feign-api, 然后引入feignstarter依赖
  • ② 将consumer-service中编写的UserClient及相关POJO类复制到feign-api模块中
  • ③ 在consumer-service中引入feign-api的依赖
  • ④ 在consumer-service新增相关配置类

注意

  在consumer-service中,当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。

  存在两种方式解决该问题,在启动类@EnableFeignClients注解上:

  • 指定FeignClient所在包(建议)

    1
    @EnableFeignClients(basePackages = "cn.xx.feign.clients")
  • 指定具体的FeignClient

    1
    @EnableFeignClients(clients = {UserClient.class})

性能优化

  Feign 底层使用的 HTTP 连接工具为URLConnection,由于其不支持连接池,性能较差,为了提升性能,我们可以更换 Feign 底层的 HTTP 连接工具为Apache HttpClient,具体集成步骤如下:

  • 引入依赖
  • 配置开启

引入依赖

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

配置开启

1
2
3
4
5
6
7
8
feign:
httpclient:
# Feign 底层使用 HttpClient
enabled: true
# 最大的连接数
max-connections: 200
# 每个路径的最大连接数
max-connections-per-route: 50

数据传递

  由于 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
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
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 重构
0%