1. 概述
在本教程中,我們將比較 Spring Feign —— 一個聲明式的 REST 客户端,以及 Spring WebClient —— 一種在 Spring 5 中引入的反應式 Web 客户端。
2. 阻塞式與非阻塞客户端
在當今的微服務生態系統中,後端服務通常需要使用 HTTP 調用其他 Web 服務。因此,Spring 應用需要一個 Web 客户端來執行這些請求。
接下來,我們將研究阻塞式 Feign 客户端和非阻塞式 WebClient 實現之間的差異。
2.1. Spring Boot 阻塞式 Feign 客户端
Feign 客户端是一個聲明式的 REST 客户端,可以簡化 Web 客户端的編寫。 使用 Feign 時,開發者只需定義接口並進行相應的註解。 實際的 Web 客户端實現則由 Spring 在運行時提供。
在幕後,標註了 <em @FeignClient 的接口會生成一個基於線程池的同步實現。 因此,對於每個請求,分配到的線程會阻塞,直到收到響應。 保持多個線程存活的缺點是,每個打開的線程都會佔用內存和 CPU 週期。
接下來,假設我們的服務受到流量激增的影響,每秒接收數千個請求。 此外,每個請求還需要等待上游服務幾秒鐘才能返回結果。
根據託管服務器分配的資源和流量激增的持續時間,一段時間後,創建的所有線程都會堆積並佔用所有分配的資源。 這種事件鏈會導致服務的性能下降,最終使服務崩潰。
2.2. Spring Boot 非阻塞式 WebClient
WebClient 是 Spring WebFlux 庫的一部分。它是由 Spring Reactive Framework 提供的一種非阻塞解決方案,旨在解決同步實現(如 Feign 客户端)的性能瓶頸。
與 Feign 客户端創建每個請求的線程並進行阻塞,直到收到響應不同,WebClient 會執行 HTTP 請求並將“等待響應”任務添加到隊列中。收到響應後,該“等待響應”任務從隊列中取出並執行,最終將響應傳遞給訂閲函數。
Reactive 框架實現了基於 Reactive Streams API 的事件驅動架構。正如我們所見,這使我們能夠編寫使用最小數量的阻塞線程執行 HTTP 請求的服務。
因此,WebClient 幫助我們構建在惡劣環境下也能保持一致性能的服務,通過使用更少的系統資源來處理更多請求。
3. 比較示例
為了查看 Feign 客户端和 <em >WebClient</em> 之間的差異,我們將實現兩個 HTTP 端點,它們都調用同一個慢端點,該端點返回一個產品列表。
我們將看到,在阻塞的 Feign 實現中,每個請求線程會在接收到響應之前阻塞兩秒。
另一方面,非阻塞的 <em >WebClient</em> 會立即關閉請求線程。
為了開始,我們需要添加三個依賴項:
<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 風格的 Web 客户端編寫方式。
首先,我們探討了 Feign 客户端,這是一種聲明式的同步阻塞 Web 客户端編寫方式。
其次,我們探討了 WebClient,它允許實現異步 Web 客户端。
儘管 Feign 客户端在許多情況下都是一個不錯的選擇,並且生成的代碼具有更低的認知複雜性,但 WebClient 的非阻塞式使用在高流量峯值期間消耗的系統資源要少得多。 考慮到這一點,對於那些需要的情況,選擇 WebClient 更加可取。