OpenFeign:让远程调用如丝般顺滑

想象一下,在我们的微服务世界里,cart-service(购物车服务)和 item-service(商品服务)是两个独立的部门。购物车部门需要查询某个商品的价格和库存,就必须去 “拜访” 一下商品部门。

在上一章,我们学会了使用 RestTemplate 这个 “信使” 来完成这次拜访。但你可能已经发现了,这个过程有点繁琐和死板:

  1. 准备拜访函:你需要手动拼接 URL,比如 http://item-service/items?ids=...。如果服务地址变了,或者参数复杂,拼接起来就像在做填字游戏,很容易出错。
  2. 明确沟通方式:你需要明确告诉 RestTemplate 是用 getForObject 还是 postForObject,就像告诉信使是送信还是取包裹。
  3. 翻译结果RestTemplate 带回来的信息(通常是 JSON 字符串),你还需要告诉它应该 “翻译” 成哪个 Java 对象,比如 ItemDTO

整个过程下来,代码显得很笨重,而且这种 “网络通信” 式的代码和我们平时写的 “本地方法调用”(比如 cartService.getCart())风格迥异,让代码的可读性和统一性大打折扣。

那么,有没有一种更优雅的方式呢?

当然有!这就是 OpenFeign 闪亮登场的时候了。

OpenFeign 的核心思想是:你只需要用 Java 接口的方式告诉我想做什么,剩下的事情都交给我。 它是一个声明式的 HTTP 客户端,我们通过简单的注解,就能将一个 Java 接口 “伪装” 成一个远程服务的代理。调用这个接口的方法,就如同调用本地方法一样简单,而 OpenFeign 则在幕后默默地为我们完成了所有复杂的网络通信工作。

让我们来看看 OpenFeign 是如何施展魔法的。一次远程调用的核心要素无非是四个:

  • 请求方式:GET、POST、PUT 还是 DELETE?
  • 请求路径:要访问哪个 URL?
  • 请求参数:需要携带哪些数据?
  • 返回值类型:期望返回什么样的数据?

OpenFeign 巧妙地利用了我们早已熟悉的 Spring MVC 注解,来声明这四大要素。


1. OpenFeign 快速入门

我们就以购物车服务(cart-service)查询商品信息为例,看看 OpenFeign 是如何让代码焕然一新的。

1.1. 引入 “装备” (添加依赖)

首先,我们需要给我们的 cart-service 配备 OpenFeign 这套 “高级装备”。在 pom.xml 文件中加入两个依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
  • spring-cloud-starter-openfeign:这是 OpenFeign 的核心启动器。
  • spring-cloud-starter-loadbalancer:这个很重要!OpenFeign 知道我们要调用 "item-service",但它不知道这个服务具体在哪台机器上(IP 和端口)。负载均衡器会去 Nacos 等服务注册中心询问,并从多个可用的 item-service 实例中智能地选择一个进行调用。

1.2. 开启 “魔法开关” (启用 OpenFeign)

光有装备还不行,我们得在项目里按下一个 “启动开关”。在 cart-service 的主启动类 CartApplication 上,添加 @EnableFeignClients 注解。

1
2
3
4
5
6
7
@SpringBootApplication
@EnableFeignClients // 开启 OpenFeign 功能
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}

这个注解告诉 Spring:“请扫描我的项目,找到所有被 @FeignClient 标记的接口,并为它们创建代理实现类。”

1.3. 编写 “契约接口” (Feign 客户端)

这是最核心的一步!我们不再编写 RestTemplate 的调用代码,而是定义一个接口,这个接口就是我们和 item-service 之间的 “通信契约”。

cart-service 中创建一个 ItemClient 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.hmall.cart.client;

import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;

@FeignClient("item-service") // 目标服务名
public interface ItemClient {

@GetMapping("/items") // 目标请求路径和方式
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); // 目标请求参数和返回值
}

让我们来解读一下这个 “契约”:

  • @FeignClient("item-service"):这是最重要的注解!它告诉 OpenFeign,这个接口下的所有方法都是用来调用名为 "item-service" 的微服务的。这个名字必须和它在 Nacos 上注册的名字完全一致。
  • @GetMapping("/items"):这和 item-service 中 Controller 层的注解一模一样。它清晰地指明了我们要发起一个 GET 请求,访问的目标路径是 /items
  • List<ItemDTO> queryItemByIds(...):方法签名部分定义了请求参数和返回值。
    • @RequestParam("ids") Collection<Long> ids:表示我们需要传递一个名为 ids 的请求参数。当我们调用这个方法时,OpenFeign 会自动将 ids 集合拼接到 URL 后面,形成 ?ids=1,2,3... 这样的形式。
    • List<ItemDTO>:这指定了我们期望的返回类型。OpenFeign 在收到 item-service 返回的 JSON 响应后,会自动帮我们反序列化成一个 List<ItemDTO> 对象。

看到了吗?我们只写了一个接口,没有写任何实现代码。但通过这些注解,OpenFeign 就掌握了所有必要的信息,它会在幕后利用动态代理技术,为我们生成一个实现了这个接口的类。

1.4. 像调用本地方法一样使用它

现在,我们可以在 CartServiceImpl 中彻底告别 RestTemplate,像注入一个普通的 Service 一样注入并使用 ItemClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

@Autowired
private ItemClient itemClient; // 直接注入 Feign 客户端

// ... 其他代码 ...

@Override
public List<CartVO> queryMyCarts() {
// ...
// 1. 获取购物车中的商品ID列表
List<Long> itemIds = carts.stream().map(Cart::getItemId).collect(Collectors.toList());

// 2. 远程调用商品服务查询商品详情
// 看!就像调用本地方法一样简单、优雅!
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);

// 3. 将 ItemDTO 转换为 VO
// ...
return voList;
}
}

对比一下之前的 RestTemplate 代码,是不是感觉清爽了无数倍?服务发现、负载均衡、HTTP 请求构建、JSON 解析…… 所有脏活累活,OpenFeign 都替我们干了。


2. 性能进阶:启用连接池

OpenFeign 底层依赖于某个 HTTP 客户端来真正地发送网络请求。默认情况下,它使用的是 Java 自带的 HttpURLConnection,这个客户端有一个缺点:不支持连接池

什么是连接池呢?

打个比方,每次远程调用就像开车去另一个城市。

  • 没有连接池:每次出门都需要重新修一条路(建立 TCP 连接),到达目的地后就把路拆掉。修路和拆路的过程非常耗时。
  • 有连接池:我们预先修好几条高速公路(保持一些 TCP 连接活跃),每次出门直接上高速,用完后也不拆,留给下一次使用。这样就大大节省了 “修路” 的时间。

因此,为了提升性能,我们通常会换用更专业的、支持连接池的 HTTP 客户端,比如 OkHttpApache HttpClient

2.1. 引入 OkHttp 依赖

cart-servicepom.xml 中,加入 OkHttp 的依赖:

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

2.2. 开启连接池配置

然后在 application.yml 文件中,添加配置来启用它:

1
2
3
feign:
okhttp:
enabled: true # 启用 feign 对 okhttp 的支持

只需这两步,重启服务后,OpenFeign 底层的通信工具就会自动从 HttpURLConnection 切换到 OkHttp,并享受连接池带来的性能提升。


3. 最佳实践:抽取 Feign 客户端

随着项目越来越大,我们会发现一个问题。cart-service 需要调用 item-service,将来新增的 trade-service(交易服务)在下单时也需要调用 item-service 来查询商品信息。

难道我们要在 trade-service 里再把 ItemClient 接口和 ItemDTO 类原封不动地复制一遍吗?这显然不符合 “不要重复你自己”(DRY)的原则。

解决方案就是:抽取!

我们将这些所有服务都可能用到的 Feign 客户端接口和相关的 DTO 对象,抽取到一个公共的 hm-api 模块中。

4.3.1. 创建公共 API 模块

我们创建一个新的 Maven 模块,比如叫 hm-api。然后将 ItemClient.javaItemDTO.javacart-service 移动到这个新模块中。

这个 hm-api 模块会非常轻量,它只包含接口和 DTO,以及 OpenFeign 的必要依赖。

3.2. 消费者服务引入 API 模块

现在,任何需要调用 item-service 的微服务(比如 cart-servicetrade-service),只需要在自己的 pom.xml 中引入 hm-api 模块的依赖即可。

1
2
3
4
5
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>

3.3. 解决包扫描问题

引入依赖后,删掉 cart-service 中原有的 ItemClientItemDTO,然后重启,你可能会遇到一个错误:ItemClient 这个 Bean 找不到了!

为什么呢?

因为 Spring Boot 默认只会扫描主启动类所在包及其子包下的组件。我们的 CartApplicationcom.hmall.cart 包下,而现在 ItemClient 被移动到了 com.hmall.api.client 包下,超出了扫描范围。

解决方法很简单,二选一:

  1. 指定扫描包:在 @EnableFeignClients 注解中明确告诉它要去哪里寻找 Feign 客户端。
1
@EnableFeignClients(basePackages = "com.hmall.api.client")
  1. 指定客户端类:更精确地指定要加载哪个 Feign 客户端。
1
@EnableFeignClients(clients = {ItemClient.class})

这样一来,代码就实现了复用,维护起来也更加方便。


4. 调试利器:配置日志

有时候远程调用失败了,或者返回的结果不符合预期,我们很想知道 OpenFeign 到底发送了一个什么样的 HTTP 请求,又收到了什么样的响应。这时,开启日志就非常有用了。

OpenFeign 的日志有四个级别:

  • NONE:不记录任何日志(默认值)。
  • BASIC:仅记录请求方法、URL、响应状态码和执行时间。
  • HEADERS:在 BASIC 基础上,额外记录请求和响应的头信息。
  • FULL:记录最详细的信息,包括头信息、请求体、元数据等,是调试时的首选。

4.1. 定义日志级别配置类

我们可以在公共的 hm-api 模块中创建一个配置类,来定义我们想要的日志级别。

1
2
3
4
5
6
7
8
9
10
11
12
package com.hmall.api.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

// 这个类可以不加 @Configuration, 如果加了就需要放到 com.hmall.api 包之外,避免被父工程扫描到
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL; // 设置日志级别为 FULL
}
}

4.2. 应用日志配置

配置好了级别,还需要告诉 OpenFeign 去使用它。同样有两种方式:

  • 局部生效:只对某一个 Feign 客户端生效。
1
2
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
public interface ItemClient { ... }
  • 全局生效:对项目中所有的 Feign 客户端都生效。
1
2
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
public class CartApplication { ... }

最后,别忘了:你还需要在 application.yml 中,将 Feign 客户端接口所在包的日志级别设置为 DEBUG,否则日志信息无法输出。

1
2
3
logging:
level:
com.hmall.api.client: debug # 将 Feign 客户端所在包的日志级别设为 debug

完成配置后,当你再次发起远程调用,就会在控制台看到详细的请求和响应日志,这对于排查问题非常有帮助!

希望这份详细的讲解能帮助你更好地理解和使用 OpenFeign!