1. 概述
本文将深入探讨Spring Reactive中的switchIfEmpty()
操作符,重点分析其在配合defer()
操作符使用时的行为差异。我们将通过实际案例演示这些操作符在不同场景下的交互方式,帮助读者理解它们对响应式流的影响。
2. switchIfEmpty()和defer()的使用场景
switchIfEmpty()
是Mono
和Flux
中的核心操作符,当原始数据源为空时,它会切换到备用数据源执行。若主数据源未发出任何数据,该操作符会自动切换到备用数据源获取数据。
考虑一个从大文件中按ID获取用户详情的接口:每次请求都遍历文件会消耗大量时间。对于高频访问的ID,使用缓存显然更高效。
当接口收到请求时:
- 首先查询缓存
- 若缓存命中,直接返回数据
- 若缓存未命中,从文件获取数据并更新缓存
在这个场景中:
- 主数据源:缓存查询流程
- 备用数据源:文件查询并更新缓存的流程
switchIfEmpty()
:根据缓存数据可用性智能切换数据源
⚠️ 关键点:defer()
操作符会延迟函数求值直到订阅发生。当switchIfEmpty()
不使用defer()
时,表达式会立即求值(急切求值),可能导致意外副作用。
3. 环境搭建
通过具体实现来分析switchIfEmpty()
在不同场景下的行为。我们将编写代码并通过系统日志判断数据来源(缓存/文件)。
3.1 数据模型
定义包含id
、name
、email
、roles
等字段的用户模型:
public class User {
@JsonProperty("id")
private String id;
@JsonProperty("email")
private String email;
@JsonProperty("username")
private String username;
@JsonProperty("roles")
private String roles;
// 标准getter和setter...
}
3.2 用户数据准备
在类路径下创建users.json
文件存储所有用户数据(JSON格式):
[
{
"id": "66b296723881ea345705baf1",
"email": "reid@example.com",
"username": "reid90",
"roles": "member"
},
{
"id": "66b29672e6f99a7156cc4ada",
"email": "boyle@example.com",
"username": "boyle94",
"roles": "admin"
},
...
]
3.3 控制器与服务实现
创建根据ID获取用户详情的控制器,通过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()
):
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();
}
当缓存未命中时,从文件获取数据并更新缓存:
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. 测试验证
使用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()
验证当withDefer=true
且缓存有数据时,仅从缓存获取数据:
@Test
void givenUserDataIsAvailableInCache_whenUserByIdIsRequestedWithDeferParameter_thenCachedResponseShouldBeRetrieved() {
usersCache = new HashMap<>();
User cachedUser = new User("66b29672e6f99a7156cc4ada", "boyle@example.com", "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\":\"boyle@example.com\",\"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")));
}
✅ 关键结论:使用defer()
时,备用数据源不会急切求值。
4.2 非空源使用switchIfEmpty()(无defer)
测试不使用defer()
时的行为:
@Test
void givenUserDataIsAvailableInCache_whenUserByIdIsRequestedWithoutDeferParameter_thenUserIsFetchedFromFileInAdditionToCache() {
usersCache = new HashMap<>();
User cachedUser1 = new User("66b29672e6f99a7156cc4ada", "boyle@example.com", "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\":\"boyle@example.com\",\"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 空源使用switchIfEmpty() + 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\":\"boyle@example.com\",\"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\":\"boyle@example.com\",\"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()
结合使用,确保仅在必要时访问备用数据源 - ❌ 常见陷阱:不使用
defer()
会导致备用数据源急切求值,产生不必要的计算和副作用 - ⚠️ 性能影响:在缓存命中场景下,不正确的实现会导致文件查询被不必要执行
简单粗暴地说:**永远在switchIfEmpty()
中使用defer()
**,这是避免性能陷阱的关键!
本文示例代码已上传至GitHub。