
spring webclient 的 timeout() 操作符默认触发 timeoutexception 并终止流,无法直接用 try-catch 捕获;正确方式是利用其带 fallback 的重载方法,在超时发生时执行自定义逻辑(如错误计数),再返回备用 mono 或抛出受控异常。
spring webclient 的 timeout() 操作符默认触发 timeoutexception 并终止流,无法直接用 try-catch 捕获;正确方式是利用其带 fallback 的重载方法,在超时发生时执行自定义逻辑(如错误计数),再返回备用 mono 或抛出受控异常。
在响应式编程中,WebClient 的链式调用构建的是一个惰性声明式流程,异常不会以传统阻塞方式“抛出并被捕获”,而是作为错误信号向下传递。因此,像 timeout(Duration) 这样的操作符触发超时时,会向下游发出 TimeoutException,但该异常发生在异步流内部,不会穿透到外层 try-catch 块——这也是你当前代码中 catch (Exception e) 无法捕获超时的根本原因。
✅ 正确做法:使用带 fallback 的 timeout() 重载
Mono.timeout(Duration, Mono<? extends T>) 允许你在超时发生时切换至一个备用 Mono。该 Mono 可用于执行副作用(如记录错误、更新策略状态),并可选择返回默认值、空值或重新抛出封装后的异常:
private Mono<GameEntity> callApplication(GameEntity gameEntity) throws URISyntaxException {
WebClient client = WebClient.create();
return Flux.range(0, NUM_OF_RETRY)
.concatMap(i -> {
String port = strategy.getNextInstancePort();
URI uri;
try {
uri = new URI(String.format("http://localhost:%s/game/add", port));
} catch (URISyntaxException e) {
return Mono.error(e);
}
return client.post()
.uri(uri)
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(gameEntity), GameEntity.class)
.retrieve()
.bodyToMono(GameEntity.class)
.timeout(
Duration.ofSeconds(3),
// ⚠️ 超时发生时执行副作用,并返回一个 Mono(此处返回 error 表示失败)
Mono.fromRunnable(() -> strategy.incrErrorCount(port))
.then(Mono.error(new RuntimeException("Request timed out for port: " + port)))
)
.onErrorResume(WebClientResponseException.class, ex -> {
strategy.incrErrorCount(port);
return Mono.error(ex);
})
.onErrorResume(Throwable.class, ex -> {
// 统一兜底:非 Web 相关异常也计入错误(如连接拒绝、DNS 失败等)
strategy.incrErrorCount(port);
return Mono.error(ex);
});
})
.next() // 取第一个成功响应
.switchIfEmpty(Mono.error(new RuntimeException("All retries failed")));
}? 关键要点说明
- Mono.fromRunnable(...).then(Mono.error(...)):确保 strategy.incrErrorCount(port) 一定被执行,且流以明确错误终止,避免静默失败;
- 避免在 timeout() fallback 中返回有效业务数据(如 Mono.just(defaultGame)),除非业务允许降级;否则应显式报错以便上游决策重试或熔断;
- onErrorResume 配合使用:不仅覆盖超时,还统一处理 HTTP 错误码(如 5xx)、网络异常等,保证所有失败路径都触发 incrErrorCount;
- 不要在循环内重复创建 WebClient:应提取为类成员变量或 Bean,提升性能与连接复用率;
- Flux.range(...).concatMap(...) 替代 for-loop:符合响应式范式,天然支持背压与异步编排,避免阻塞线程。
? 总结
WebClient 超时不是“被抛出的异常”,而是“流中的错误事件”。与其试图用阻塞式思维捕获它,不如用响应式方式响应它:通过 timeout(Duration, fallback) 主动接管超时控制权,并结合 onErrorResume 实现健壮的错误统计与恢复策略。这是构建高可用响应式微服务调用的关键实践。

















