1. 概述

在这个教程中,我们将比较Spring的两种客户端:Feign(一个声明式REST客户端)和Spring 5引入的WebClient(一个反应式网络客户端)。在现代微服务架构中,后端服务通常需要通过HTTP调用其他Web服务,因此Spring应用需要一个网络客户端来执行请求。

接下来,我们将分析阻塞Feign客户端与非阻塞WebClient之间的差异。

2. 阻塞与非阻塞客户端

在当今的微服务生态系统中,通常需要后台服务使用HTTP调用其他Web服务。因此,Spring应用需要一个网络客户端来进行这些请求。

2.1. Spring Boot 阻塞Feign客户端

Feign客户端是一种声明式的REST客户端,它简化了编写网络客户端的过程。使用Feign时,开发人员只需定义接口并进行相应的注解。然后,Spring会在运行时提供实际的网络客户端实现。

在幕后,被@FeignClient注解的接口会基于每个请求一个线程的模型生成同步实现。因此,对于每个请求,分配的线程会阻塞直到收到响应。保持多个线程活跃的缺点是每个打开的线程都会占用内存和CPU周期。

设想我们的服务受到流量高峰的影响,每秒接收到数千个请求。除此之外,每个请求还需要等待上游服务返回结果数秒。

根据托管服务器的资源分配和流量高峰的持续时间,一段时间后,创建的所有线程将开始堆积并占用所有分配的资源。最终,这一系列事件会导致服务性能下降,甚至导致服务崩溃。

2.2. Spring Boot 非阻塞WebClient

WebClient是Spring WebFlux库的一部分。它是Spring反应式框架提供的解决方案,旨在解决像Feign客户端这样的同步实现性能瓶颈。

当Feign客户端为每个请求创建一个线程并在收到响应之前阻塞时,WebClient执行HTTP请求,并将“等待响应”任务添加到队列中。随后,在收到响应后,从队列中执行“等待响应”任务,最后将响应交付给订阅函数。

反应式框架采用由Reactive Streams API驱动的事件驱动架构。如我们所见,这使我们能够使用最少的阻塞线程执行HTTP请求。

因此,WebClient帮助我们构建在严苛环境中表现一致的服务,使用更少的系统资源处理更多请求。

3. 对比示例

为了观察Feign客户端和WebClient之间的差异,我们将实现两个HTTP端点,它们都调用同一个返回产品列表的慢端点。

我们将看到,在阻塞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); // delay
    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实现第一个端点:

首先,定义接口并使用@FeignClient注解:

@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!

正如预期的那样,在同步实现中,请求线程会等待接收所有产品。之后,它会将它们打印到控制台,然后退出控制器函数,最后关闭请求线程。

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;
}

控制器函数不再返回产品列表,而是返回一个Flux发布者并迅速完成方法。在这种情况下,消费者会订阅Flux实例,并在产品可用时处理它们。

再次查看日志:

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)

正如预期的那样,控制器函数立即完成,从而也完成了请求线程。一旦产品可用,订阅的函数就会处理它们。

4. 总结

在这篇文章中,我们比较了Spring中编写网络客户端的两种风格。

首先,我们探讨了Feign客户端,这是一种编写同步阻塞网络客户端的声明式方式。

其次,我们研究了WebClient,它允许实现网络客户端的异步版本。

尽管Feign客户端在许多情况下是一个很好的选择,代码的认知复杂性较低,但在高流量峰值期间,非阻塞的WebClient使用更少的系统资源。考虑到这一点,在这些情况下,选择WebClient更为可取。

如往常一样,本文的代码可以在GitHub上找到。