在 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 上找到。