Feign解决了什么问题

一个服务里要调用别的HTTP接口,每次要手写一些重复的HTTP处理逻辑,比如我们如果单独用Ribbon调用别的服务,就需要每次写这样一坨代码

String result = restTemplate.getForObject("http://ServiceName/InterfacePath", String.class);
虽然Spring的RestTemplate已经将HTTP调用的代码成本降到很低了,但如果极致一些,完全不写行不行?答案是可以。Feign就是提我们完成这件事情的不二之选。

快速体验Feign

此次我们依然在以前的示例上添加代码,这样就可以将精力放到体会Feign的特性上了。

1、给ServiceA加几个接口

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@PostMapping("/")
public String createUser(@RequestBody User user) {
Integer port = environment.getProperty("local.server.port", Integer.class);
System.out.println("创建用户," + user);
return "{'msg': 'success', 'from': " + port + "}";
}

@PutMapping("/{id}")
public String updateUser(@PathVariable("id") Long id, @RequestBody User user) {
System.out.println("更新用户,id:" + id);
Integer port = environment.getProperty("local.server.port", Integer.class);
return "{'msg': 'success', 'from': " + port + "}";
}

@DeleteMapping("/{id}")
public String deleteUser(@PathVariable("id") Long id) {
System.out.println("删除用户,id:" + id);
Integer port = environment.getProperty("local.server.port", Integer.class);
return "{'msg': 'success', 'from': " + port + "}";
}

@GetMapping("/{id}")
public User getById(@PathVariable("id")Long id) {
System.out.println("查询用户,id:" + id);
return new User("sam", 25, id);
}

static class User {
private String username;
private Integer age;
private Long id;

public User(String username, Integer age, Long id) {
this.username = username;
this.age = age;
this.id = id;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}

2、重构ServiceB:基于Feign调用ServiceA

在serviceB的pom.xml中添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在启动类中添加@FeignClients注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@EnableFeignClients(basePackages = {"cc.houqian.eurekalearn.client"})
@EnableEurekaClient
@SpringBootApplication
public class ServiceBApplication {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}

public static void main(String[] args) {
SpringApplication.run(ServiceBApplication.class, args);
log.info("ServiceB 启动成功");
}
}

新建一个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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@FeignClient("serviceA")
public interface ServiceAClient {

@GetMapping("/sayHello/{name}")
String sayHello(@PathVariable("name") String name);

@PostMapping("/")
String createUser(@RequestBody User user);

@PutMapping("/{id}")
String updateUser(@PathVariable("id") Long id,
@RequestBody User user);

@DeleteMapping("/{id}")
String deleteUser(@PathVariable("id") Long id);

@GetMapping("/{id}")
User getById(@PathVariable("id") Long id);

static class User {
private String username;
private Integer age;
private Long id;

public User(String username, Integer age, Long id) {
this.username = username;
this.age = age;
this.id = id;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
}

改造接口调用处,使用Feign客户端

1
2
3
4
5
6
7
8
9
10
    @Resource
private ServiceAClient serviceAClient;

@GetMapping("/greeting/{name}")
public String greeting(@PathVariable("name") String name) {
// String response = restTemplate.getForObject("http://serviceA/sayHello/" + name, String.class);
String response = serviceAClient.sayHello(name);
log.info(response);
return response;
}

打开浏览器,访问http://localhost:9000/greeting/sam,你会发现与之前使用RestTemplate时的效果一致

基于Feign调用服务A

此处功能已经实现,我们可以再想多一些,还有什么问题?
如果serviceA有多个调用方serviceC、D等等,按照现在这个套路,ServiceAClient这个Feign客户端是得每个服务都来一遍,非常不优雅。

我们可以想一下,提供服务的serviceA肯定是最了解自己的,根据知识专家原则,由他来提供feign客户端供其他消费者调用时合情合理的,这也解决了代码复用的问题。

所以,我们此时完全可以将ServiceAClient挪到一个新的工程里去,由serviceA自己维护,里面的职责就是维护serviceA一系列的暴露出去的feign客户端,然后让其他服务依赖这个工程,就可以消费serviceA的接口了。

3、Feign的继承特性

在上面,我们讨论了一个更好的做法。其实,Feign能提供的编码体验更为极致,我们上面还有一处代码是重复的,就是serviceA暴露接口的SpringMVC注解代码,现在使用Feign的继承特性,我们只需要定义一处SpringMVC的注解逻辑就可以了,这提供了极致的编码体验,非常的Clean。

(1)单独定义一个工程 service-a-api,定义统一对外暴露的接口和实体类

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<artifactId>service-a-api</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

服务A对外暴露接口

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package cc.houqian.eurekalearn.servicea.client;

// 注意此处不加@RestController
public interface ServiceAInterface {

@GetMapping("/sayHello/{name}")
String sayHello(@PathVariable("name") String name);

@PostMapping("/")
String createUser(@RequestBody User user);

@PutMapping("/{id}")
String updateUser(@PathVariable("id") Long id,
@RequestBody User user);

@DeleteMapping("/{id}")
String deleteUser(@PathVariable("id") Long id);

@GetMapping("/{id}")
User getById(@PathVariable("id") Long id);

static class User {
private String username;
private Integer age;
private Long id;

public User(String username, Integer age, Long id) {
this.username = username;
this.age = age;
this.id = id;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
}

(2)ServiceA对定义好的接口实现业务逻辑

在serviceA的pom.xml中新增依赖

1
2
3
4
5
<dependency>
<groupId>cc.houqian</groupId>
<artifactId>service-a-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

改造ServiceAController

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
28
29
30
31
32
33
34
@RestController
public class ServiceAController implements ServiceAInterface {

@Resource
private Environment environment;

public String sayHello(@PathVariable("name") String name) {
Integer port = environment.getProperty("local.server.port", Integer.class);
return "hello," + name + ", came from " + port;
}

public String createUser(@RequestBody User user) {
Integer port = environment.getProperty("local.server.port", Integer.class);
System.out.println("创建用户," + user);
return "{'msg': 'success', 'from': " + port + "}";
}

public String updateUser(@PathVariable("id") Long id, @RequestBody User user) {
System.out.println("更新用户,id:" + id);
Integer port = environment.getProperty("local.server.port", Integer.class);
return "{'msg': 'success', 'from': " + port + "}";
}

public String deleteUser(@PathVariable("id") Long id) {
System.out.println("删除用户,id:" + id);
Integer port = environment.getProperty("local.server.port", Integer.class);
return "{'msg': 'success', 'from': " + port + "}";
}

public User getById(@PathVariable("id") Long id) {
System.out.println("查询用户,id:" + id);
return new User("sam", 25, id);
}
}

(3)改造ServiceB依赖ServiceA的接口Jar包

在serviceB的pom.xml中新增依赖

1
2
3
4
5
<dependency>
<groupId>cc.houqian</groupId>
<artifactId>service-a-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

改造ServiceAClient

1
2
3
4
5
6
7
8
9
package cc.houqian.eurekalearn.client;

import cc.houqian.eurekalearn.servicea.client.ServiceAInterface;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient("serviceA")
public interface ServiceAClient extends ServiceAInterface {

}

(4)实验

使用POSTMAN,随意调用接口,这里以createUser为例,可以看到基于Feign继承功能的调用是OK的。

Feign继承功能调用结果

Feign的自定义配置

Feign提供了请求拦截器的功能,当我们调用的服务,有共性的需求需要处理时,非常适合使用拦截器实现,比如Token获取等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class MyConf {
@Bean
public RequestInterceptor requestInterceptor() {
return new MyRequestInterceptor();
}
}

@Slf4j
public static class MyRequestInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
log.info("url:{}", template.url());
log.info("variables:{}", template.variables());
log.info("method:{}", template.method());
log.info("headers:{}", template.headers());
log.info("body:{}", template.bodyTemplate());
// 这里非常适合获取token之类的操作
// 比如某个三方接口,需要token之类的东西才能访问
// 你就在这里把token获取到,放到header里就OK了
}
}

Feign拦截器实验

核心组件

笔者觉得核心组件这种东西完全可以放到入门篇里,因为这是你了解一门技术的必经之路,哪怕入门也是有必要了解的。
下面这张图摘录自https://github.com/OpenFeign/feign#feature-overview

Feign核心组件

  • Client(客户端)
    在SpringCloud环境里就是FeignClient,我们使用Feign最核心的入口。其实,Spring就是帮我们构造了一个FeignClient,里面包含了各种核心组件,比如EncoderDecoderLoggerContract等待
  • Contract(协议)
    意思是说除了Feign原生的那些API外,他还对各种其他技术做了集成,这个集成好像一个个协议,将Feign的核心API与别的技术连接起来,进行通信。
    以Spring为例,Feign的核心API肯定不会识别Spring web mvc的各种注解,比如@PatchVariable、@RequestMapping、@RequestParam等,但他通过Spring4或者SpringBoot这些为三方库定制的协议,就可以将Spring的这么些个web注解解释成自己的API了,让人家不用改变自己的使用习惯就可以使用Feign。
  • Encoder、Decoder(编码器、解码器)
    其实就是你调用接口的时候,传递的参数是个对象,feign需要将这个对象进行encode,编码,比如转成一个JSON格式,这就是Encoder的职责。
    Decoder,解码器,就是你的接口收到一个JSON字符串后,Feign给你转成你定义的对象
  • Feign.Builder(图中未列出)
    Feign Core提供的一个构造器,用来构造一个FeignClient实例
  • Logger(图中未列出)
    Feign Core提供的日志组件

SpringCloud提供的Feign默认组件

笔者在前面Eureka、Ribbon的系列文章里,已经详细的剖析过SpringCloud整合三方技术的套路了,此处笔者不再赘述,直接给出对应的结论。

  • Spring提供的默认FeignClient是LoadBalancerFeignClient
  • Spring提供的默认解码器是ResponseEntityDecoder,带有分页功能的解码器是PageableSpringEncoder
  • Spring提供的默认编码器是SpringEncoder
  • Spring提供的默认协议是SpringMvcContract
  • Spring提供的默认构造器是Feign.Builder(这个就是Feign Core自带的,原生的)
  • Spring提供的默认日志组件是Slf4jLogger

常见配置

超时相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
feign:
client:
config:
serviceA:
connectionTimeout: 500
readTimeout: 1000
loggerLevel: full
decode404: false
defaul:
connectionTimeout: 500
readTimeout: 1000
loggerLevel: full
decode404: false

压缩相关配置

1
2
3
4
5
6
7
8
9
feign:
compression:
request:
enabled: true
mime-types: text/xml.application/xml,application/json
min-request-size: 2048
response:
enabled: true
useGzipDecoder: false

日志相关配置

1
2
# 此处配合feign的日志级别使用
logging.level.cc.houqian.eurekalearn.client.ServiceAClient=debug
1
2
3
4
5
6
7
8
9
10
public static class MyConf {
@Bean
public Logger.Level feignLoggerLevel() {
// NONE: 不打印日志
// BASIC: 只打印请求方法类型、URL和返回状态码、执行时间
// HEADERS: 在BASIC的基础上,附加请求、响应的header
// FULL: 打印所有信息
return Logger.Level.FULL;
}
}

效果

Feign的FULL日志级别打印效果

总结

本篇我们了解如下内容

  • Feign的核心概念
  • SpringCloud环境下Feign的使用方法
    • Feign的继承功能很好用,开发中经常使用
  • SpringCloud环境下Feign的默认组件
  • SpringCloud环境下Feign的配置

Comments