1. 簡介
Spring WebFlux 是一個反應式編程框架,它支持異步、非阻塞的通信。 使用 WebFlux 的關鍵在於處理 Mono 對象,該對象代表一個單一的異步結果。 在實際應用中,我們經常需要將一個 Mono 對象轉換為另一個 Mono 對象,無論是為了豐富數據、處理外部服務調用,還是重構報文負載。
在本次教程中,我們將探討如何使用 Project Reactor 提供的各種方法將 Mono 對象轉換為另一個 Mono 對象。
2. 轉換 單 個對象
在探討各種將 單 個對象轉換的方法之前,我們先設置我們的編碼示例。我們將使用圖書借閲示例貫穿整個教程,以演示不同的轉換方法。為了捕捉這個場景,我們將使用三個關鍵類。
User 類來表示圖書館用户:
public class User {
private String userId;
private String name;
private String email;
private boolean active;
// standard setters and getters
}每個用户都通過 userId 這一唯一標識符進行識別,並且擁有姓名和電子郵件等個人信息。此外,還有一個 active 標誌,用於指示用户當前是否可以借閲書籍。
Book 類用於表示圖書館的書籍收藏:
public class Book {
private String bookId;
private String title;
private double price;
private boolean available;
//standard setters and getters
}每個圖書都通過 bookId 進行標識,並具有諸如 title 和 price 等屬性。 available 標誌指示圖書是否可以借閲。
BookBorrowResponse 類用於封裝借閲操作的結果:
public class BookBorrowResponse {
private String userId;
private String bookId;
private String status;
//standard setters and getters
}本類將 userId 和 bookId 在流程中關聯起來,並提供一個 status 字段,用於指示借閲是否被接受或拒絕。
3. 隨行轉換與 <em >map()</em >>
<em >map()</em >> 運算符將同步函數應用於 <em >Mono</em >> 內的數據。它適用於諸如格式化、過濾或簡單計算等輕量級操作。例如,如果我們想要從 <em >Mono</em >> 中的用户對象獲取電子郵件地址,可以使用 <em >map()</em >> 進行轉換:
@Test
void givenUserId_whenTransformWithMap_thenGetEmail() {
String userId = "U001";
Mono<User> userMono = Mono.just(new User(userId, "John", "[email protected]"));
Mockito.when(userService.getUser(userId))
.thenReturn(userMono);
Mono<String> userEmail = userService.getUser(userId)
.map(User::getEmail);
StepVerifier.create(userEmail)
.expectNext("[email protected]")
.verifyComplete();
}4. 使用 <em flatMap()</em> 進行異步轉換
<em flatMap()</em> 方法將每個從 <em Mono</em> 發出的項目轉換為另一個 <em Publisher</em>。它尤其適用於需要啓動新的異步流程的轉換,例如調用另一個 API 或查詢數據庫。<em flatMap()</em> 在轉換結果是 <em Mono</em> 時,會將結果扁平化為一個單一序列。
讓我們來看一下我們的圖書借閲系統。當用户請求借閲圖書時,系統會驗證用户的會員狀態,然後檢查圖書是否可用。如果這兩個檢查都通過,系統將處理借閲請求並返回一個 <em BookBorrowResponse</em>:
public Mono<BookBorrowResponse> borrowBook(String userId, String bookId) {
return userService.getUser(userId)
.flatMap(user -> {
if (!user.isActive()) {
return Mono.error(new RuntimeException("User is not an active member"));
}
return bookService.getBook(bookId);
})
.flatMap(book -> {
if (!book.isAvailable()) {
return Mono.error(new RuntimeException("Book is not available"));
}
return Mono.just(new BookBorrowResponse(userId, bookId, "Accepted"));
});
}在本示例中,如檢索用户信息和圖書詳情等操作,都是異步的,並返回 Mono 對象。 通過使用 flatMap(), 我們可以以可讀且邏輯的方式鏈接這些操作,而無需嵌套多個級別的 Mono。 序列中的每個步驟都依賴於前一個步驟的結果。 例如,圖書可用性僅在用户處於活動狀態時才進行檢查。 flatMap() 確保我們可以在保持流的反應式特性不變的情況下,動態地做出這些決策。
5. 使用 transform() 方法實現可複用邏輯
transform() 方法是一個多功能的工具,允許我們封裝可複用的邏輯。與其在應用程序的多個部分中重複執行轉換,不如一次定義它們並根據需要隨時應用。 這促進了代碼重用、分層架構和可讀性。
讓我們來看一個例子,其中我們需要在應用税費和折扣後返回書籍的最終價格:
public Mono<Book> applyDiscount(Mono<Book> bookMono) {
return bookMono.map(book -> {
book.setPrice(book.getPrice() - book.getPrice() * 0.2);
return book;
});
}
public Mono<Book> applyTax(Mono<Book> bookMono) {
return bookMono.map(book -> {
book.setPrice(book.getPrice() + book.getPrice() * 0.1);
return book;
});
}
public Mono<Book> getFinalPricedBook(String bookId) {
return bookService.getBook(bookId)
.transform(this::applyTax)
.transform(this::applyDiscount);
}在本示例中,applyDiscount()方法應用20%的折扣,applyTax()方法則應用10%的税費。transform方法將這兩個方法應用於流水線,並返回一個包含最終價格的Mono對象,類型為Book。
6. 從多個來源合併數據
zip() 方法將多個 Mono 對象組合起來,產生一個單一結果。 它不進行併發合併,而是等待所有 Mono 對象發出數據後才應用組合器函數。
我們再次回顧我們的圖書借閲示例,其中我們獲取用户信息和圖書信息以創建 BookBorrowResponse:
public Mono<BookBorrowResponse> borrowBookZip(String userId, String bookId) {
Mono userMono = userService.getUser(userId)
.switchIfEmpty(Mono.error(new RuntimeException("User not found")));
Mono bookMono = bookService.getBook(bookId)
.switchIfEmpty(Mono.error(new RuntimeException("Book not found")));
return Mono.zip(userMono, bookMono,
(user, book) -> new BookBorrowResponse(userId, bookId, "Accepted"));
}在本實現中,zip() 方法確保在創建響應之前,用户信息和圖書信息可用。如果用户或圖書檢索失敗(例如,如果用户不存在或圖書不可用),則錯誤將傳播,並導致 Mono 對象以適當的錯誤信號終止。
7. 條件轉換
通過結合 <em >filter()</em > 和 <em >switchIfEmpty()</em >> 方法,我們可以為 <em >Mono</em >> 對象應用條件邏輯,根據謂詞進行轉換。如果謂詞為真,則返回原始的 <em >Mono</em >> 對象,否則 <em >switchIfEmpty()</em >> 方法會將其切換到由其提供的一個不同 <em >Mono</em >> 對象,反之亦然。
讓我們考慮一個場景,即如果用户處於活動狀態,則僅應用折扣,否則不提供折扣:
public Mono<Book> conditionalDiscount(String userId, String bookId) {
return userService.getUser(userId)
.filter(User::isActive)
.flatMap(user -> bookService.getBook(bookId).transform(this::applyDiscount))
.switchIfEmpty(bookService.getBook(bookId))
.switchIfEmpty(Mono.error(new RuntimeException("Book not found")));
}在本示例中,我們使用 Mono 類型的 User 對象,通過 userId 進行獲取。 過濾方法檢查用户是否處於活動狀態。 如果用户處於活動狀態,我們將應用折扣後返回一個 Mono 類型的 Book 對象。 如果用户處於非活動狀態,Mono 將變為空,switchIfEmpty() 方法啓動,用於在不應用折扣的情況下獲取書籍。 最終,如果書籍本身不存在,另一個 switchIfEmpty() 確保適當的錯誤被傳播,使整個流程具有魯棒性和直觀性。
8. 轉換過程中的錯誤處理
錯誤處理確保轉換過程中的彈性,允許採用優雅的降級機制或替代數據源。當轉換失敗時,適當的錯誤處理有助於優雅地恢復、記錄問題或返回替代數據。
onErrorResume() 方法用於通過提供替代的 <em Mono</em> 來從錯誤中恢復。這在我們需要提供默認數據或從替代源獲取數據時尤其有用。
讓我們重新審視我們的圖書借閲示例:如果在獲取 <em User</em> 或 <em Book</em> 對象時發生任何錯誤,我們通過返回具有“已拒絕”狀態的 <em BookBorrowResponse</em> 對象來優雅地處理失敗:
public Mono<BookBorrowResponse> handleErrorBookBorrow(String userId, String bookId) {
return borrowBook(userId, bookId)
.onErrorResume(ex -> Mono.just(new BookBorrowResponse(userId, bookId, "Rejected")));
}這種錯誤處理策略確保,即使在故障場景下,系統也能以可預測的方式響應,並保持無縫的用户體驗。
9. 轉換單對象最佳實踐
在轉換 Mono 對象時,遵循一些最佳實踐對於確保我們的反應式管道乾淨、高效和易於維護至關重要。當我們需要簡單的同步轉換,例如豐富或修改數據時,map() 方法是理想的選擇,而 flatMap() 則適用於涉及異步工作流的任務,例如調用外部 API 或查詢數據庫。為了保持管道的清潔和可重用性,我們使用 transform() 方法封裝邏輯,從而促進模塊化和分層設計。為了保持可讀性,我們應優先使用鏈式操作而非嵌套操作。
錯誤處理在確保健壯性方面發揮着關鍵作用。通過使用諸如 onErrorResume() 等方法,我們可以優雅地處理錯誤,通過提供備用響應或替代數據源。最後,在每個階段驗證輸入和輸出有助於防止問題向下傳播,從而確保管道的健壯性和可擴展性。
10. 結論
在本教程中,我們學習瞭如何將一個 <em >Mono</em> 對象轉換為另一個對象的各種方法。 重要的是要理解正確的運算符,無論它是否為 <em >map()</em>、<em >flatMap()</em> 或 <em >transform()</em>。 通過使用這些技術並遵循最佳實踐,我們可以構建在 Spring WebFlux 中靈活且易於維護的反應式流水線。