1. 概述
本文將重點介紹 Spring Reactive 中 <a href="https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html#switchIfEmpty-reactor.core.publisher.Mono-"><em >switchIfEmpty()</em></em ></a > 操作符及其與defer() 操作符的交互行為。我們將探討這些操作符在不同場景下的交互方式,並通過實際示例説明它們對反應式流的影響。
2. 使用 <em>switchIfEmpty()</em> 和 <em>Defer()</em>
switchIfEmpty() 是 Mono 和 Flux 中的一個運算符,當源生產者為空時,執行備用生產者流。如果主源發佈器沒有發出任何數據,此運算符將切換到來自備用源的數據流。
考慮一個通過 ID 從大型文件中檢索用户詳細信息的端點。每次有人從文件中請求用户詳細信息時,迭代文件會消耗大量時間。因此,頻繁訪問的 ID 最好緩存其詳細信息。
當端點收到請求時,我們首先在緩存中搜索。如果用户詳細信息可用,則返回響應。如果不可用,則從文件中檢索數據並將其緩存以供後續請求使用。
在此上下文中,主要的數據提供者是檢查鍵是否存在於緩存中的流,而備用方案是檢查鍵在文件中以及更新緩存的流。`switchIfEmpty()` 運算符可以根據緩存中數據的可用性高效地切換源提供者。
同時,瞭解 `defer() 運算符的使用也很重要,該運算符延遲或推遲函數評估,直到發生訂閲。
如果不使用 `switchIfEmpty()` 運算符與 `defer()` 運算符一起使用,則表達式會立即評估(急切評估),這可能會導致意外的副作用。
3. 設置
讓我們進一步探索這個示例,以瞭解 <em >switchIfEmpty()</em> 運算符在不同情況下的行為。
我們將實現必要的代碼,並分析系統日誌,以確定我們是否從緩存中獲取用户,或者從文件中獲取用户。
3.1. 數據模型
首先,我們定義一個用户模型,其中包含一些信息,例如 id、姓名、電子郵件、角色:
public class User {
@JsonProperty("id")
private String id;
@JsonProperty("email")
private String email;
@JsonProperty("username")
private String username;
@JsonProperty("roles")
private String roles;
// standard getters and setters...
}3.2. 用户數據設置
隨後,請在類路徑中維護一個文件(users.json),其中包含所有用户詳細信息,格式為 JSON:
[
{
"id": "66b296723881ea345705baf1",
"email": "[email protected]",
"username": "reid90",
"roles": "member"
},
{
"id": "66b29672e6f99a7156cc4ada",
"email": "[email protected]",
"username": "boyle94",
"roles": "admin"
},
...
]3.3. 控制器和服務實現
在下一步中,讓我們添加一個控制器,用於通過 ID 檢索用户詳細信息。它將接受一個可選的布爾參數 <em withDefer,並根據此查詢參數採用不同的實現:
@GetMapping("/user/{id}")
public Mono<ResponseEntity<User>> findUserDetails(@PathVariable("id") String id,
@RequestParam("withDefer") boolean withDefer) {
return (withDefer ? userService.findByUserIdWithDefer(id) :
userService.findByUserIdWithoutDefer(id)).map(ResponseEntity::ok);
}然後,讓我們在 UserService 中定義這兩個實現,分別帶有和不帶 defer(),以便理解 switchIfEmpty(): 的行為。
public Mono<User> findByUserIdWithDefer(String id) {
return fetchFromCache(id).switchIfEmpty(Mono.defer(() -> fetchFromFile(id)));
}public Mono<User> findByUserIdWithoutDefer(String id) {
return fetchFromCache(id).switchIfEmpty(fetchFromFile(id));
}為了簡化,我們來實現一個內存緩存,作為主要的數據源來保留用户信息,用於處理請求。同時,我們還會記錄每次訪問,這使得我們可以確定緩存是否檢索到了數據:
private final Map<String, User> usersCache;
private Mono<User> fetchFromCache(String id) {
User user = usersCache.get(id);
if (user != null) {
LOG.info("Fetched user {} from cache", id);
return Mono.just(user);
}
return Mono.empty();
}隨後,當ID不在緩存中時,從文件中檢索用户信息,並在找到數據時更新緩存:
private Mono<User> fetchFromFile(String id) {
try {
File file = new ClassPathResource("users.json").getFile();
String usersData = new String(Files.readAllBytes(file.toPath()));
List<User> users = objectMapper.readValue(usersData, new TypeReference<List<User>>() {
});
User user = users.stream()
.filter(u -> u.getId()
.equalsIgnoreCase(id))
.findFirst()
.get();
usersCache.put(user.getId(), user);
LOG.info("Fetched user {} from file", id);
return Mono.just(user);
} catch (IOException e) {
return Mono.error(e);
}
}請注意記錄日誌以驗證用户數據是否從文件中檢索到。
4. 測試
讓我們在 BeforeEach 測試方法中添加一個 ListAppender,用於跟蹤日誌。我們將使用它來確定緩存或文件是否針對不同的請求執行該函數:
protected ListAppender<ILoggingEvent> listAppender;
@BeforeEach
void setLogger() {
Logger logger = (Logger) LoggerFactory.getLogger(UserService.class);
logger.setLevel(Level.DEBUG);
listAppender = new ListAppender<>();
logger.addAppender(listAppender);
listAppender.start();
}我們可以在以下部分添加一些測試,以驗證各種條件。
4.1. 使用 switchIfEmpty() 和在非空源上 defer()
我們將驗證實現僅在請求帶有 `` 參數設置為 `` 時從緩存中檢索用户數據,並相應地斷言日誌輸出。
@Test
void givenUserDataIsAvailableInCache_whenUserByIdIsRequestedWithDeferParameter_thenCachedResponseShouldBeRetrieved() {
usersCache = new HashMap<>();
User cachedUser = new User("66b29672e6f99a7156cc4ada", "[email protected]", "boyle94", "admin");
usersCache.put("66b29672e6f99a7156cc4ada", cachedUser);
userService.getUsers()
.putAll(usersCache);
webTestClient.get()
.uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=true")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"," +
"\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");
assertTrue(listAppender.list.stream()
.anyMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
assertTrue(listAppender.list.stream()
.noneMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from file")));
}當使用 switchIfEmpty() 方法與 defer() 運算符結合使用時,替代源提供者不會被急促地評估。
4.2. 在非空源上不使用 switchIfEmpty() 運算符和 defer()
讓我們添加另一個測試,以檢查不使用 switchIfEmpty() 運算符和 defer() 運算符的行為:
@Test
void givenUserDataIsAvailableInCache_whenUserByIdIsRequestedWithoutDeferParameter_thenUserIsFetchedFromFileInAdditionToCache() {
usersCache = new HashMap<>();
User cachedUser1 = new User("66b29672e6f99a7156cc4ada", "[email protected]", "boyle94", "admin");
usersCache.put("66b29672e6f99a7156cc4ada", cachedUser1);
userService.getUsers().putAll(usersCache);
webTestClient.get()
.uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=false")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"," +
"\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");
assertTrue(listAppender.list.stream()
.anyMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from file")));
assertTrue(listAppender.list.stream()
.anyMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
}如我們所見,該實現從緩存和文件同時獲取了用户信息,但最終仍然從緩存中返回了響應。備選源中的代碼塊是不必要的觸發,儘管主要源(緩存)已經產生了輸出。
4.3. 使用 defer() 處理空源的 <em>switchIfEmpty()</em>
隨後,我們添加一個測試,以驗證在緩存中沒有數據時,用户詳情是從文件中檢索的,特別是當使用 `defer()` 運算符時:
@Test
void givenUserDataIsNotAvailableInCache_whenUserByIdIsRequestedWithDeferParameter_thenFileResponseShouldBeRetrieved() {
webTestClient.get()
.uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=true")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"
,\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");
assertTrue(listAppender.list.stream()
.anyMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from file")));
assertTrue(listAppender.list.stream()
.noneMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
}API 從文件獲取用户信息,按照預期執行,而不是嘗試從緩存中檢索用户信息。
4.4. switchIfEmpty() 在空源上不使用 defer()
最後,讓我們添加一個測試,以驗證在不使用 defer() 的情況下,實現從文件檢索用户詳細信息(即緩存中沒有數據)是否有效:
@Test
void givenUserDataIsNotAvailableInCache_whenUserByIdIsRequestedWithoutDeferParameter_thenFileResponseShouldBeRetrieved() {
webTestClient.get()
.uri("/api/v1/user/66b29672e6f99a7156cc4ada?withDefer=false")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("{\"id\":\"66b29672e6f99a7156cc4ada\"," + "\"email\":\"[email protected]\",\"username\":\"boyle94\",\"roles\":\"admin\"}");
assertTrue(listAppender.list.stream()
.anyMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from file")));
assertTrue(listAppender.list.stream()
.noneMatch(e -> e.toString()
.contains("Fetched user 66b29672e6f99a7156cc4ada from cache")));
}由於緩存中沒有數據,用户信息不會從緩存中獲取,但會嘗試作為副作用獲取,但 API 仍然按照預期從文件中獲取用户信息。
5. 結論
在本文中,我們重點研究了 Spring Reactive 中 switchIfEmpty() 操作符及其通過測試的各種行為。
當使用 switchIfEmpty() 與 defer() 結合使用時,可以確保僅在必要時才訪問備用數據源。這可以防止不必要的計算和潛在的副作用。