知識庫 / Spring WebFlux RSS 訂閱

理解 Spring Reactive 中 switchIfEmpty() 的用法

Spring WebFlux
HongKong
7
10:58 AM · Dec 06 ,2025

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()MonoFlux 中的一個運算符,當源生產者為空時,執行備用生產者流。如果主源發佈器沒有發出任何數據,此運算符將切換到來自備用源的數據流。

考慮一個通過 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() 結合使用時,可以確保僅在必要時才訪問備用數據源。這可以防止不必要的計算和潛在的副作用。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.