1. 概述
WebClient 是一個簡化執行 HTTP 請求過程的接口。與 RestTemplate 不同,它是一個反應式且非阻塞的客户端,可以消費和操作 HTTP 響應。雖然它被設計為非阻塞的,但也可以在阻塞場景中使用。
在本教程中,我們將深入研究 WebClient 接口中的關鍵方法,包括 retrieve()、exchangeToMono() 和 exchangeToFlux()。我們還將探討這些方法之間的差異和相似之處,並提供示例以展示不同的用例。此外,我們還將使用 JSONPlaceholder API 獲取用户數據。
2. 示例配置
首先,讓我們創建一個 Spring Boot 應用程序,並添加 spring-boot-starter-webflux 依賴項到 pom.xml 中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.2.4</version>
</dependency>
這個依賴項提供WebClient接口,使我們能夠執行HTTP請求。
此外,讓我們來看一個從請求https://jsonplaceholder.typicode.com/users/1返回的示例GET響應:
{
"id": 1,
"name": "Leanne Graham",
// ...
}此外,讓我們創建一個名為 User 的 POJO 類:
class User {
private int id;
private String name;
// standard constructor,getter, and setter
}來自 JSONPlaceholder API 的 JSON 響應將被反序列化並映射到 User 類的一個實例。
最後,我們創建一個 WebClient 實例,並設置基本 URL:
WebClient client = WebClient.create("https://jsonplaceholder.typicode.com/users");這裏,我們定義了 HTTP 請求的基URL。
3. exchange() 方法
exchange() 方法直接返回 ClientResponse 對象,從而提供對 HTTP 狀態碼、頭部和響應體訪問。 簡單來説,ClientResponse 代表由 WebClient 返回的 HTTP 響應。
然而,自 Spring 5.3 版本起,此方法已被棄用,並被 exchangeToMono() 或 exchangeToFlux() 方法取代,具體取決於我們發出的內容。這兩個方法允許我們根據響應狀態解碼響應。
3.1. 發射 Mono 對象
下面是一個使用 exchangeToMono() 方法發射 Mono 對象的示例:
@GetMapping("/user/exchange-mono/{id}")
Mono<User> retrieveUsersWithExchangeAndError(@PathVariable int id) {
return client.get()
.uri("/{id}", id)
.exchangeToMono(res -> {
if (res.statusCode().is2xxSuccessful()) {
return res.bodyToMono(User.class);
} else if (res.statusCode().is4xxClientError()) {
return Mono.error(new RuntimeException("Client Error: can't fetch user"));
} else if (res.statusCode().is5xxServerError()) {
return Mono.error(new RuntimeException("Server Error: can't fetch user"));
} else {
return res.createError();
}
});
}在上述代碼中,我們檢索用户並根據 HTTP 狀態碼解碼響應。
3.2. 發出 Flux 信號
此外,我們使用 exchangeToFlux() 方法來獲取用户集合:
@GetMapping("/user-exchange-flux")
Flux<User> retrieveUsersWithExchange() {
return client.get()
.exchangeToFlux(res -> {
if (res.statusCode().is2xxSuccessful()) {
return res.bodyToFlux(User.class);
} else {
return Flux.error(new RuntimeException("Error while fetching users"));
}
});
}在這裏,我們使用 exchangeToFlux() 方法將響應體映射為 Flux 中 User 對象,並在請求失敗時返回自定義錯誤消息。
3.3. 直接獲取響應體
值得注意的是,exchangeToMono() 或 exchangeToFlux() 可以無需指定響應狀態碼進行使用:
@GetMapping("/user-exchange")
Flux<User> retrieveAllUserWithExchange(@PathVariable int id) {
return client.get().exchangeToFlux(res -> res.bodyToFlux(User.class))
.onErrorResume(Flux::error);
}在這裏,我們檢索用户,不指定狀態碼。
3.4. 修改響應體
此外,我們來看一個修改響應體的示例:
@GetMapping("/user/exchange-alter/{id}")
Mono<User> retrieveOneUserWithExchange(@PathVariable int id) {
return client.get()
.uri("/{id}", id)
.exchangeToMono(res -> res.bodyToMono(User.class))
.map(user -> {
user.setName(user.getName().toUpperCase());
user.setId(user.getId() + 100);
return user;
});
}在上述代碼中,在將響應體映射到POJO類後,我們通過在id中添加100和將name轉換為大寫來修改響應體。
特別地,我們還可以使用retrieve()方法來修改響應體。
3.5. 提取響應頭信息
此外,我們還可以提取響應頭信息:
@GetMapping("/user/exchange-header/{id}")
Mono<User> retrieveUsersWithExchangeAndHeader(@PathVariable int id) {
return client.get()
.uri("/{id}", id)
.exchangeToMono(res -> {
if (res.statusCode().is2xxSuccessful()) {
logger.info("Status code: " + res.headers().asHttpHeaders());
logger.info("Content-type" + res.headers().contentType());
return res.bodyToMono(User.class);
} else if (res.statusCode().is4xxClientError()) {
return Mono.error(new RuntimeException("Client Error: can't fetch user"));
} else if (res.statusCode().is5xxServerError()) {
return Mono.error(new RuntimeException("Server Error: can't fetch user"));
} else {
return res.createError();
}
});
}在這裏,我們記錄 HTTP 頭部和內容類型到控制枱。 與需要返回 ResponseEntity才能訪問頭部和響應碼的 retrieve()方法不同,exchangeToMono()由於它返回 ClientResponse,因此我們能夠直接訪問。
4. retrieve() 方法
retrieve() 方法簡化了從 HTTP 請求中提取響應體。它返回 ResponseSpec,允許我們指定響應體應如何處理,而無需訪問完整的 ClientResponse。
ClientResponse 包含響應代碼、頭信息和響應體。因此,ResponseSpec 包含響應體,但不包含響應代碼和頭信息。
4.1. 發出 單聲道 數據包
以下是一個用於檢索 HTTP 響應體示例代碼:
@GetMapping("/user/{id}")
Mono<User> retrieveOneUser(@PathVariable int id) {
return client.get()
.uri("/{id}", id)
.retrieve()
.bodyToMono(User.class)
.onErrorResume(Mono::error);
}在上述代碼中,我們通過向 /users 終點進行 HTTP 調用,並指定一個 id,從基礎 URL 獲取 JSON 數據。然後,我們將響應體映射到 User 對象。
4.2. 發送 Flux</em/> 請求
此外,讓我們來看一個通過向 /users</em/> 終點發送 GET 請求的示例:
@GetMapping("/users")
Flux<User> retrieveAllUsers() {
return client.get()
.retrieve()
.bodyToFlux(User.class)
.onResumeError(Flux::error);
}在此,該方法在將 HTTP 響應映射到 POJO 類時,會發出 Flux 類型的 User 對象流。
4.3. 返回 ResponseEntity
當我們使用 retrieve() 方法來訪問響應狀態和頭部時,我們可以返回 ResponseEntity:
@GetMapping("/user-id/{id}")
Mono<ResponseEntity<User>> retrieveOneUserWithResponseEntity(@PathVariable int id) {
return client.get()
.uri("/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(User.class)
.onErrorResume(Mono::error);
}使用 toEntity()方法獲得的響應包含 HTTP 標頭、狀態碼和響應正文。
4.4. 自定義錯誤使用 <em onStatus()> 處理程序
此外,當發生 400 或 500 HTTP 錯誤時,默認返回 異常。但是,我們可以使用 處理程序自定義異常,從而提供自定義錯誤響應。
@GetMapping("/user-status/{id}")
Mono<User> retrieveOneUserAndHandleErrorBasedOnStatus(@PathVariable int id) {
return client.get()
.uri("/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
response -> Mono.error(new RuntimeException("Client Error: can't fetch user")))
.onStatus(HttpStatusCode::is5xxServerError,
response -> Mono.error(new RuntimeException("Server Error: can't fetch user")))
.bodyToMono(User.class);
}在這裏,我們檢查HTTP狀態碼,並使用onStatus()處理程序來定義自定義錯誤響應。
5. 性能比較
接下來,我們編寫一個性能測試,以使用 Java Microbench Harness (JMH) 比較 retrieve() 和 exchangeToFlux() 的執行時間。
首先,讓我們創建一個名為 RetrieveAndExchangeBenchmarkTest 的類:
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 10, timeUnit = TimeUnit.MICROSECONDS)
@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.MICROSECONDS)
public class RetrieveAndExchangeBenchmarkTest {
private WebClient client;
@Setup
public void setup() {
this.client = WebClient.create("https://jsonplaceholder.typicode.com/users");
}
}在這裏,我們設置基準模式為 AverageTime,這意味着它會測量測試執行的平均時間。此外,我們還定義了迭代次數和每個迭代的時間。
接下來,我們創建了一個 WebClient 的實例,並使用 @Setup 註解使其在每次基準測試之前運行。
讓我們編寫一個基準方法,使用 retrieve() 方法檢索用户集合:
@Benchmark
Flux<User> retrieveManyUserUsingRetrieveMethod() {
return client.get()
.retrieve()
.bodyToFlux(User.class)
.onErrorResume(Flux::error);;
}最後,讓我們定義一個方法,該方法使用 exchangeToFlux() 方法,以 Flux 形式發出 User 對象:
@Benchmark
Flux<User> retrieveManyUserUsingExchangeToFlux() {
return client.get()
.exchangeToFlux(res -> res.bodyToFlux(User.class))
.onErrorResume(Flux::error);
}以下是基準測試結果:
Benchmark Mode Cnt Score Error Units
retrieveManyUserUsingExchangeToFlux avgt 15 ≈ 10⁻⁴ s/op
retrieveManyUserUsingRetrieveMethod avgt 15 ≈ 10⁻³ s/op兩種方法都展示了高效的性能。但是,exchangeToFlux()在檢索用户集合時,比 retrieve() 方法略快。
6. 關鍵差異與相似之處
retrieve() 和 exchangeToMono() 或 exchangeToFlux() 都可以用於發起 HTTP 請求並提取 HTTP 響應。
retrieve() 方法僅允許我們消費 HTTP 響應體併發出 Mono 或 Flux,因為它返回 ResponseSpec。 但是,如果我們想訪問狀態碼和頭部信息,可以使用 retrieve() 方法與 ResponseEntity 一起使用。 此外,它還允許我們根據 HTTP 狀態碼使用 onStatus() 處理程序報告錯誤。
與 retrieve() 方法不同,exchangeToMono() 和 exchnageToFlux() 允許我們消費 HTTP 響應並直接訪問頭部信息和響應碼,因為它們返回 ClientResponse。 此外,它們還提供了更精細的錯誤處理控制,因為我們可以根據 HTTP 狀態碼解碼響應。
值得注意的是,如果意圖是僅消費響應體,則建議使用 retrieve() 方法。
但是,如果我們需要對響應進行更精細的控制,exchangeToMono() 或 exchangeToFlux() 可能是更好的選擇。
7. 結論
在本文中,我們學習瞭如何使用 retrieve()、exchangeToMono() 和 exchangeToFlux() 方法來處理 HTTP 響應,並將其映射到 POJO 類。此外,我們還比較了 retrieve() 和 exchangeToFlux() 方法的性能。
retrieve() 方法適用於我們只需要消費響應主體,而不需要訪問狀態碼或標頭的情況。它通過返回 ResponseSpec,簡化了處理響應體過程。