OpenFeign 最佳实践
# OpenFeign 最佳实践
前言
在使用 Feign 时,服务消费者的 Feign 客户端与服务提供者的 Controller 代码往往非常相似,比如接口路径、参数定义、方法声明等。为减少代码重复,我们可以通过以下两种优化方式:继承方式和抽取方式。
# 1. 问题描述
以下是 Feign 客户端和服务提供者的代码对比:
- Feign 客户端:
- UserController:
可以看到,这两部分代码非常相似。有没有办法简化这种重复的代码呢?接下来介绍两种优化方法。
# 2. 通过继承实现代码复用
我们可以通过将相同的代码抽取到一个公共接口,服务提供者和消费者(Feign 客户端)都继承该接口,从而减少重复代码。
实现步骤
- 定义公共 API 接口:在接口中声明服务提供者和消费者共用的方法。
- 服务提供者的 Controller 和 Feign 客户端都继承该接口。
# 代码实现
公共 API 接口:
package com.example.api;
import com.example.pojo.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 公共 API 接口,用于定义服务接口
*/
public interface UserApi {
/**
* 通过用户 ID 获取用户信息
* @param id 用户 ID
* @return 用户信息
*/
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
说明:
UserApi
接口中使用了 SpringMVC 注解定义了接口路径、请求方法和参数类型。该接口可以被服务提供者和消费者共用。
服务提供者(Controller):
package com.example.controller;
import com.example.api.UserApi;
import com.example.pojo.User;
import org.springframework.web.bind.annotation.RestController;
/**
* 服务提供者,继承 UserApi 接口
*/
@RestController
public class UserController implements UserApi {
@Override
public User findById(Long id) {
// 模拟数据库查询,返回用户信息
return new User(id, "张三");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
说明:
UserController
继承了UserApi
接口,因此服务提供者只需实现接口中的方法,而不需要重新定义路径和注解。
Feign 客户端:
package com.example.feign;
import com.example.api.UserApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* Feign 客户端,继承 UserApi 接口
*/
@FeignClient("userservice")
public interface UserClient extends UserApi {
// Feign 客户端不需要额外的实现,只需继承 UserApi 即可
}
2
3
4
5
6
7
8
9
10
11
12
说明:
UserClient
通过继承UserApi
接口,实现了与服务提供者一致的接口定义,同时通过@FeignClient("userservice")
注解指定要调用的服务名称。
# 继承的优点
- 代码复用性高:统一接口定义,减少重复代码。
- 一致性强:服务提供者和消费者之间的接口一致,避免了定义不一致导致的问题。
# 继承的缺点
- 耦合性高:服务提供者和消费者之间存在一定的耦合。
- 注解不继承:参数列表中的注解不会被继承,因此 Controller 中需要重新声明注解。
# 3. 通过抽取公共模块实现代码复用
在微服务架构中,多个服务之间常常需要通过 Feign 实现远程调用。如果每个服务都分别定义 Feign 客户端接口、数据对象(POJO )类和相关配置,必然会导致大量的重复代码。随着项目规模扩大,代码的重复性和维护成本会显著增加。因此,进行公共模块的抽取是一个有效的解决方案。
# 1. 为什么要进行抽取?
将 Feign 客户端、数据对象(POJO)和配置抽取到一个独立的模块,带来的好处包括:
- 代码复用性高:所有服务都可以依赖同一个模块,避免重复编写相同的 Feign 客户端和数据对象。
- 维护成本低:公共模块中的代码一旦需要更新,只需在公共模块中修改,所有依赖该模块的服务都会自动更新。
- 一致性强:通过抽取公共模块,所有服务的 Feign 调用方式、数据结构和配置保持一致,减少了代码冗余和错误的可能性。
# 2. 实现步骤
实现步骤
- 创建公共模块
feign-api
:用于存放公共接口、POJO 和配置。 - 将 Feign 客户端接口、POJO 和配置移动到
feign-api
模块中。 - 服务消费者引入
feign-api
依赖,直接复用公共模块中的接口和配置。
# 2.1 创建公共模块 feign-api
首先,创建一个新的模块 feign-api
,用于存放公共的 Feign 接口、POJO 类和配置。
feign-api 模块结构:
在这个模块中,我们将 Feign 客户端接口、数据对象(POJO)和配置进行集中管理。
feign-api 中的依赖:
在 pom.xml
中添加 Feign 的依赖,使该模块可以使用 Feign 相关的注解和功能:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2
3
4
# 2.2 定义公共 API 接口
在 feign-api
模块中定义服务提供者和消费者共用的 API 接口:
package com.example.api;
import com.example.pojo.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 公共 API 接口,用于定义服务接口
*/
public interface UserApi {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
2
3
4
5
6
7
8
9
10
11
12
13
代码说明:这个接口使用了 SpringMVC 的注解,定义了服务的路径、请求方法和参数格式。服务提供者的 Controller 和消费者的 Feign 客户端都可以继承这个接口,从而避免重复代码。
# 2.3 定义 Feign 客户端接口
在 feign-api
模块中定义 Feign 客户端接口,并让其继承公共的 API 接口:
package com.example.feign;
import com.example.api.UserApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 公共模块中的 Feign 客户端接口
*
* @FeignClient 注解指定要调用的服务名称 "userservice"
*/
@FeignClient("userservice")
public interface UserClient extends UserApi {
// Feign 客户端不需要额外的实现,只需继承 UserApi 即可
}
2
3
4
5
6
7
8
9
10
11
12
13
14
代码说明:
@FeignClient("userservice")
:指定调用的服务名称为userservice
,Feign 会自动去注册中心查找该服务并发起调用。- 通过继承
UserApi
,可以将 Feign 客户端的定义与服务提供方保持一致,减少代码重复。
# 2.4 引入公共模块并使用 Feign
在需要使用该 Feign 客户端的服务中(如 order-service
),引入 feign-api
模块的依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
2
3
4
5
在 order-service
中使用 Feign:
package com.example.order.service;
import com.example.feign.UserClient;
import com.example.pojo.User;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class OrderService {
@Autowired
private UserClient userClient; // 引入抽取后的 Feign 客户端
/**
* 通过用户 ID 查询用户信息
* @param id 用户 ID
* @return 用户信息
*/
public User queryUserById(Long id) {
// 使用 Feign 客户端发起远程调用,查询用户信息
return userClient.findById(id);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
代码说明:在业务服务中,通过注入 Feign 客户端接口即可完成远程调用。所有相关代码已经在
feign-api
模块中统一管理,服务消费者只需简单调用即可。
# 3. 解决 Feign 扫描问题
由于 UserClient
现在位于 feign-api
模块的包下,可能不在服务消费者的默认扫描路径中。可以通过以下方式解决:
方式一:指定扫描包路径:
@EnableFeignClients(basePackages = "com.example.feign")
方式二:指定需要加载的 Feign 客户端接口:
@EnableFeignClients(clients = {UserClient.class})
# 4. 抽取后的调用示例
抽取后,使用 Feign 客户端的方法在业务逻辑中调用非常简单:
package com.example.order.controller;
import com.example.order.service.OrderService;
import com.example.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/order/{userId}")
public User getUserById(@PathVariable Long userId) {
// 调用 OrderService 中的方法,通过 Feign 客户端获取用户信息
return orderService.queryUserById(userId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在浏览器中访问 http://localhost:8080/order/101
即可看到结果。
结论
通过抽取公共模块,可以有效减少代码重复,提升代码复用性。这种方式在多微服务项目中尤为适用,能够大幅降低维护成本,提高代码一致性。