在 Spring WebFlux 中將 Mono 物件轉換為另一個 Mono 物件
一、簡介
Spring WebFlux 是一個反應式程式框架,可促進非同步、非阻塞式通訊。使用 WebFlux 的一個關鍵方面是處理Mono對象,表示單一非同步結果。在現實應用程式中,我們經常需要將一個Mono物件轉換為另一個對象,無論是豐富資料、處理外部服務呼叫或重組有效負載。
在本教程中,我們將探索如何使用 Project Reactor 提供的各種方法將Mono物件轉換為另一個Mono物件。
2. 轉換Mono對象
在我們探索轉換Mono物件的各種方法之前,讓我們先設定我們的編碼範例。我們將在整個教程中使用借書範例來示範不同的轉換方法。為了捕捉這種情況,我們將使用三個關鍵類別。
代表圖書館使用者的User類別:
public class User {
private String userId;
private String name;
private String email;
private boolean active;
// standard setters and getters
}
每個使用者都由userId唯一標識,並擁有name和email等個人詳細資料。此外,還有一個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連結在一起,並提供一個狀態欄位來指示借閱是被接受還是被拒絕。
3. 使用map()進行同步轉換
map運算子將同步函數應用於Mono內的資料。它適合格式化、過濾或簡單計算等輕量級操作。例如,如果我們想要取得Mono用戶的電子郵件地址,我們可以使用map來轉換它:
@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. 使用flatMap()進行非同步轉換
flatMap()方法將每個發出的項目從Mono轉換為另一個Publisher 。當轉換需要新的非同步過程(例如進行另一個 API 呼叫或查詢資料庫)時,它特別有用。當轉換結果是Mono時, flatMap()將結果展平為單一序列。
讓我們看看我們的圖書借閱系統。當使用者要求借書時,系統會驗證使用者的會員身份,然後檢查該書是否可用。如果兩項檢查都通過,系統將處理借閱請求並傳回BookBorrowResponse :
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% 的稅費。轉換方法在管道中應用這兩種方法,並傳回Book的Mono和最終價格。
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. 條件轉換
透過組合filter()和switchIfEmpty()方法,我們可以應用條件邏輯來基於謂詞轉換Mono物件。如果謂詞為 true,則傳回原始 Mono,如果為 false,則Mono切換到switchIfEmpty()提供的其他 Mono,反之亦然。
讓我們考慮一個場景,我們只想在用戶活躍時應用折扣,否則返回無折扣:
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")));
}
在此範例中,我們使用userId來取得User的Mono 。過濾器方法檢查使用者是否處於活動狀態。如果使用者處於活躍狀態,我們會在套用折扣後傳回Mono的Book 。如果使用者處於非活動狀態,Mono 就會變空,並且switchIfEmpty()方法會啟動以獲取書籍而不應用折扣。最後,如果書本身不存在,另一個switchIfEmpty()確保傳播適當的錯誤,使整個流程穩健且直觀。
8. 轉換期間的錯誤處理
錯誤處理確保轉換的彈性,允許優雅的回退機製或替代資料來源。當轉換失敗時,正確的錯誤處理有助於正常復原、記錄問題或傳回替代資料。
onErrorResume()方法用於透過提供替代Mono來從錯誤中恢復。當我們想要提供預設資料或從替代來源取得資料時,這特別有用。
讓我們回顧一下借書的例子;如果在取得User或Book物件時拋出任何錯誤,我們會透過傳回狀態為「Rejected」的BookBorrowResponse物件來優雅地處理失敗:
public Mono<BookBorrowResponse> handleErrorBookBorrow(String userId, String bookId) {
return borrowBook(userId, bookId)
.onErrorResume(ex -> Mono.just(new BookBorrowResponse(userId, bookId, "Rejected")));
}
這種錯誤處理策略確保即使在故障情況下,系統也能做出可預測的回應並保持無縫的使用者體驗。
9. 轉換 Mono 物件的最佳實踐
在轉換Mono物件時,必須遵循一些最佳實踐,以確保我們的反應式管道乾淨、高效且可維護。當我們需要簡單、同步的轉換(例如豐富或修改資料)時, map()方法是完美的選擇,而flatMap()則非常適合涉及非同步工作流程的任務,例如呼叫外部 API 或查詢資料庫。為了保持管道的清潔和可重複使用性,我們使用transform()方法封裝邏輯,從而促進模組化和關注點分離。為了保持可讀性,我們應該更喜歡連結而不是嵌套操作。
錯誤處理在確保彈性方面發揮關鍵作用。透過使用像onErrorResume()這樣的方法,我們可以透過提供後備回應或替代資料來源來優雅地管理錯誤。最後,在每個階段驗證輸入和輸出有助於防止問題向下游傳播,確保管道穩健且可擴展。
10. 結論
在本教程中,我們學習了將一個Mono物件轉換為另一個物件的各種方法。了解適合該作業的正確運算子非常重要,無論是map() 、 flatMap()或transform() 。透過這些技術並應用最佳實踐,我們可以在 Spring WebFlux 中建立靈活且可維護的反應式管道。
與往常一樣,本文中使用的所有程式碼片段都可以在 GitHub 上找到。