
本文详解为何在使用 flatMap 递归调用 deleteAccount() 时出现方法被触发两次的问题,核心原因在于测试中对 Mono.block() 的重复调用导致下游逻辑(如 accountRegistry.getSessions())被执行两次,并提供基于 StepVerifier 的正确异步测试方案与代码重构建议。
本文详解为何在使用 `flatmap` 递归调用 `deleteaccount()` 时出现方法被触发两次的问题,核心原因在于测试中对 `mono.block()` 的重复调用导致下游逻辑(如 `accountregistry.getsessions()`)被执行两次,并提供基于 `stepverifier` 的正确异步测试方案与代码重构建议。
在 Reactor 编程模型中,Mono 是一个惰性、可订阅的异步容器——它本身不执行任何逻辑,直到被显式订阅(例如通过 .block()、.subscribe() 或测试框架触发)。你遇到的“deleteAccount() 被调用两次”问题,并非 flatMap 本身导致的重复调度,而是测试代码中对同一 Mono 实例连续调用了两次 .block(),每次调用都会触发一次完整的链式执行:
var res = sut.deleteAccount(accountId); assertEquals(res.block().getDeletedSessions().contains(accountId)); // ← 第一次 block() → 触发 accountRegistry.getSessions() assertEquals(1, res.block().getDeletedSessions().size()); // ← 第二次 block() → 再次触发 accountRegistry.getSessions()
由于 accountRegistry.getSessions() 是有副作用的操作(如清理会话),两次 block() 就表现为“被调用两次”,从而让 verify(accountRegistry).getSessions(...) 断言失败。
✅ 正确做法:始终对 Mono/Flux 使用 StepVerifier 进行响应式测试,它仅订阅一次,并支持断言值、异常、背压等行为:
import reactor.test.StepVerifier;
@Test
void deleteAccount_whenActive_thenDeletesSessionsOnce() {
String accountId = "test-123";
StepVerifier.create(sut.deleteAccount(accountId))
.expectNextMatches(response ->
response.getDeletedSessions().contains(accountId) &&
response.getDeletedSessions().size() == 1)
.verifyComplete();
// 单次验证即可覆盖所有副作用
verify(accountRegistry).getSessions(eq(List.of(accountId)));
}
⚠️ 注意事项:
- ❌ 避免在测试或生产代码中对同一
Mono多次调用.block()—— 它不是缓存,而是重放整个计算流程; - ✅ 若需复用结果,应使用
.cache()(注意内存与线程安全)或提前map提取所需字段; - ? 递归调用
deleteAccount(accountId)在flatMap中是合法的,但需确保终止条件可靠(如isActive状态变更),否则可能引发无限递归(尽管本例中未体现); - ? 补充建议:将
accountRegistry.getSessions(...)的副作用逻辑封装为Mono<list>></list>,使整个链路真正响应式、可组合、易测:
// 改进后的 deleteAccount(更清晰、无隐藏阻塞)
public Mono<response> deleteAccount(String accountId) {
return accountDataProvider.accountDataFor(accountId)
.flatMap(accountData -> {
if (accountData.isActive) {
// 副作用转为 Mono,保持响应式流完整性
return accountRegistry.getSessions(List.of(accountId))
.map(sessions -> {
Response response = new Response();
sessions.forEach(response::addToDeletedSessions);
return response;
});
}
return Mono.just(new Response());
});
}</response>
总结:Reactor 的核心原则是「声明式 + 惰性执行」。测试时务必用 StepVerifier 替代手动 block();设计时优先将 I/O 和副作用包装为 Mono,而非在 map 中直接调用阻塞方法——这既是避免重复执行的关键,也是构建高可靠性响应式服务的基础。











