Spring 中的 FeignClient 与 WebClient

1、概览

在本教程中,我们将对 Spring Feign(声明式 REST 客户端)和 Spring WebClient(Spring 5 中引入的响应式 Web 客户端)的优缺点进行比较。

2、阻塞式和非阻塞式客户端

在微服务系统中,服务之间的通常通过 RESTful API 进行通信。因此,需要一个 Web 客户端来执行请求。

接下来,我们将了解阻塞式 Feign 客户端与非阻塞式 WebClient 之间的区别。

2.1、阻塞式 Feign 客户端

Feign 客户端是一种声明式 REST 客户端,能快速、简单地开发 Web 客户端。使用 Feign 时,只需定义接口并对其进行相应注解。然后,Spring 会在运行时生成实际的 Web 客户端实现。

@FeignClient 的实现使用 “每个请求一个线程” 的同步模型。因此,对于每个请求,当前线程会阻塞,直到收到响应为止。如果同时发起多个请求,就要使用大量的线程,每个打开的线程都会占用内存和CPU。

2.2、非阻塞式 WebClient 客户端

WebClient 是 Spring WebFlux 的一部分。它是 Spring Reactive Framework 提供的一种非阻塞解决方案,用于解决 Feign 客户端等同步实现的性能瓶颈。

Feign 客户端会为每个请求创建一个线程并阻塞它,直到收到响应为止,而 WebClient 会执行 HTTP 请求并将 “等待响应” 任务添加到队列中。随后,在收到响应后,从队列中执行 “等待响应” 任务,最后将响应发送给 Subscriber (订阅者)函数。

Reactive 框架实现了由 Reactive Streams API 支持的事件驱动架构。正如我们所看到的,这使我们能够编写只需要最少数量的阻塞线程就能执行 HTTP 请求的服务。

因此,WebClient 可以使用更少的系统资源处理更多的请求,从而帮助我们构建能够在资源紧张环境中稳定运行的服务。

3、对比示例

为了了解 Feign 客户端和 WebClient 之间的区别,我们将实现两个 HTTP 端点,它们都会调用同一个返回 Product(产品)列表的耗时端点。

在阻塞式 Feign 实现中,每个请求线程都会阻塞两秒,直到收到响应为止。

非阻塞式 WebClient 线程会在请求执行后立即返回。

首先,添加三个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

接下来是耗时端点定义:

@GetMapping("/slow-service-products")
private List<Product> getAllProducts() throws InterruptedException {
    Thread.sleep(2000L); // 模拟耗时2秒
    return Arrays.asList(
      new Product("Fancy Smartphone", "A stylish phone you need"),
      new Product("Cool Watch", "The only device you need"),
      new Product("Smart TV", "Cristal clean images")
    );
}

3.1、使用 Feign 调用耗时服务

使用 Feign 实现第一个端点。

定义接口并注解 @FeignCleint

@FeignClient(value = "productsBlocking", url = "http://localhost:8080")
public interface ProductsFeignClient {

    @RequestMapping(method = RequestMethod.GET, value = "/slow-service-products", produces = "application/json")
    List<Product> getProductsBlocking(URI baseUrl);
}

使用 ProductsFeignClient 接口来调用耗时服务:

@GetMapping("/products-blocking")
public List<Product> getProductsBlocking() {
    log.info("Starting BLOCKING Controller!");
    final URI uri = URI.create(getSlowServiceBaseUri());

    List<Product> result = productsFeignClient.getProductsBlocking(uri);
    result.forEach(product -> log.info(product.toString()));

    log.info("Exiting BLOCKING Controller!");
    return result;
}

执行请求,查看日志:

Starting BLOCKING Controller!
Product(title=Fancy Smartphone, description=A stylish phone you need)
Product(title=Cool Watch, description=The only device you need)
Product(title=Smart TV, description=Cristal clean images)
Exiting BLOCKING Controller!

正如预期的那样,在同步实现的情况下,请求线程正在等待接收所有 Product。之后,它会将 Product 打印到控制台,并退出 Controller 方法,最后释放请求线程。

3.2、使用 WebClient 调用耗时服务

接着,实现一个非阻塞式的 WebClient 来调用同一个端点:

@GetMapping(value = "/products-non-blocking", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Product> getProductsNonBlocking() {
    log.info("Starting NON-BLOCKING Controller!");

    Flux<Product> productFlux = WebClient.create()
      .get()
      .uri(getSlowServiceBaseUri() + SLOW_SERVICE_PRODUCTS_ENDPOINT_NAME)
      .retrieve()
      .bodyToFlux(Product.class);

    productFlux.subscribe(product -> log.info(product.toString()));

    log.info("Exiting NON-BLOCKING Controller!");
    return productFlux;
}

Controller 方法不是返回 Product 列表,而是返回 Flux Publisher,并立即结束当前方法。在这种情况下,Consumer 将订阅 Flux 实例,并在 Product 可用时对其进行处理。

查看请求日志:

Starting NON-BLOCKING Controller!
Exiting NON-BLOCKING Controller!
Product(title=Fancy Smartphone, description=A stylish phone you need)
Product(title=Cool Watch, description=The only device you need)
Product(title=Smart TV, description=Cristal clean images)

不出所料,Controller 方法立即返回,请求线程线程不会阻塞。一旦 Product 可用,订阅函数就会对其进行处理。

4、总结

在本文中,我们比较了在 Spring 中编写 Web 客户端的两种风格。

总的来说,Feign 较为简单、省心,更能专注于业务,但是对系统资源占用比较高。而 WebClient 恰好相反,资源占用较少,性能高,但是响应式编程风格比较繁琐,所有操作都是异步的,都需要通过回调或者监听来实现。

没有银弹,你可以根据自己需求、场景不同选择合适的实现。


参考:https://www.baeldung.com/spring-boot-feignclient-vs-webclient