1. 概述
在本教程中,我們將比較 Spring 兩個 Web Client 實現——<em >RestTemplate</em> 和 Spring 5 引入的反應式替代方案 <em >WebClient</em>。
2. 阻塞式與非阻塞客户端
在 Web 應用程序中,向其他服務發出 HTTP 調用是很常見的需求。因此,我們需要一個 Web 客户端工具。
2.1. RestTemplate 阻塞式客户端
Spring 長期以來一直提供 RestTemplate 作為 Web 客户端的抽象。在底層,RestTemplate 使用 Java Servlet API,該 API 基於線程一請求一線程(thread-per-request)模型。
這意味着線程將阻塞,直到 Web 客户端接收到響應。由於每個線程消耗一定數量的內存和 CPU 週期,因此阻塞代碼的問題在於這樣。
讓我們考慮有大量傳入請求,這些請求正在等待某個慢速服務生成結果。
遲早,等待結果的請求會堆積起來。 因此,應用程序將創建許多線程,從而耗盡線程池或佔用所有可用內存。 我們還會因為頻繁的 CPU 上下文(線程)切換而導致性能下降。
2.2. 非阻塞 WebClient
WebClient 使用了 Spring Reactive 框架提供的異步、非阻塞解決方案。
雖然 WebTemplate 使用調用線程處理每個事件(HTTP 調用),WebClient 會為每個事件創建一個類似“任務”。 在幕後,Reactive 框架會排隊這些“任務”,並在適當的響應可用時才執行它們。
Reactive 框架使用事件驅動架構。 它通過 Reactive Streams API 提供了一種組合異步邏輯的手段。 因此,與同步/阻塞方法相比,反應式方法可以在使用更少的線程和系統資源的同時處理更多邏輯。
WebClient 是 Spring WebFlux 庫的一部分。 因此,我們也可以使用具有反應式類型(Mono 和 Flux)的函數式、流暢 API 來編寫客户端代碼,以聲明式的方式進行組合。
3. 示例對比
為了展示這兩種方法之間的差異,我們需要運行帶有大量併發客户端請求的性能測試。
使用阻塞方法後,隨着並行客户端請求數量的增加,性能會顯著下降。
然而,反應式/非阻塞方法在無論請求數量多少的情況下都應該保持穩定的性能。
對於本文,我們將實現兩個 REST 端點,一個使用 <em >RestTemplate</em>,另一個使用 <em >WebClient</em>。 它們的任務是調用另一個慢速 REST Web 服務,該服務返回一個包含推文的列表。
為了開始,我們需要以下 Spring Boot WebFlux starter 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>以下是我們的慢速服務 REST 端點:
@GetMapping("/slow-service-tweets")
private List<Tweet> getAllTweets() {
Thread.sleep(2000L); // delay
return Arrays.asList(
new Tweet("RestTemplate rules", "@user1"),
new Tweet("WebClient is better", "@user2"),
new Tweet("OK, both are useful", "@user1"));
}3.1. 使用 RestTemplate 調用慢速服務
現在,我們將實現另一個 REST 端點,該端點將通過 Web 客户端調用我們的慢速服務。
首先,我們將使用 RestTemplate:
@GetMapping("/tweets-blocking")
public List<Tweet> getTweetsBlocking() {
log.info("Starting BLOCKING Controller!");
final String uri = getSlowServiceUri();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<List<Tweet>> response = restTemplate.exchange(
uri, HttpMethod.GET, null,
new ParameterizedTypeReference<List<Tweet>>(){});
List<Tweet> result = response.getBody();
result.forEach(tweet -> log.info(tweet.toString()));
log.info("Exiting BLOCKING Controller!");
return result;
}當我們調用此端點時,由於 RestTemplate 的同步特性,代碼會阻塞等待來自慢服務的迴應。該方法中其餘代碼只有在收到響應後才會執行。
以下是我們將在日誌中看到的:
Starting BLOCKING Controller!
Tweet(text=RestTemplate rules, username=@user1)
Tweet(text=WebClient is better, username=@user2)
Tweet(text=OK, both are useful, username=@user1)
Exiting BLOCKING Controller!3.2. 使用 WebClient 調用慢速服務
第二,我們使用 WebClient 調用慢速服務:
@GetMapping(value = "/tweets-non-blocking",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Tweet> getTweetsNonBlocking() {
log.info("Starting NON-BLOCKING Controller!");
Flux<Tweet> tweetFlux = WebClient.create()
.get()
.uri(getSlowServiceUri())
.retrieve()
.bodyToFlux(Tweet.class);
tweetFlux.subscribe(tweet -> log.info(tweet.toString()));
log.info("Exiting NON-BLOCKING Controller!");
return tweetFlux;
}在這種情況下,WebClient 返回一個 Flux 訂閲者,並且方法執行完成。當結果可用時,訂閲者將開始向其訂閲者發出推文。
請注意,一個客户端(在本例中為 Web 瀏覽器)調用此 /tweets-non-blocking 端點也會訂閲返回的 Flux 對象。
現在讓我們觀察一下日誌:
Starting NON-BLOCKING Controller!
Exiting NON-BLOCKING Controller!
Tweet(text=RestTemplate rules, username=@user1)
Tweet(text=WebClient is better, username=@user2)
Tweet(text=OK, both are useful, username=@user1)請注意,該端點方法在接收到響應之前就已經完成。
4. 結論
在本文中,我們探討了兩種使用 Web 客户端在 Spring 中的方法。
<em style="font-style: italic;">RestTemplate</em> 使用 Java Servlet API,因此是同步且阻塞式的。
相反,<em style="font-style: italic;">WebClient</em> 是異步的,並且在等待響應返回時不會阻塞執行線程。<strong style="font-style: italic;">通知僅在響應準備好時才會產生。</strong>
<em style="font-style: italic;">RestTemplate</em> 仍然將被使用。但是,在某些情況下,非阻塞式方法與阻塞式方法相比,使用的系統資源要少得多。因此,在這些情況下,<em style="font-style: italic;">WebClient</em> 是更優的選擇。