You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

非REST场景下Spring WebClient异步调用异常的捕获处理方法

问题背景
  • 定时任务触发Spring WebClient非阻塞异步Mono调用,不属于REST接口请求链路
  • 异常触发时会直接抛出并打印完整堆栈,期望仅捕获异常后完成优雅日志记录,不输出未处理异常堆栈
  • 常规异常处理方案均不适用:
    • 外层包裹try/catch无法捕获异步Mono流内部抛出的异常
    • 公开资料中WebClient异步全局异常捕获方案仅适配REST请求触发的调用场景
    • AOP方案无法切入异步Mono的异常执行节点

现有实现代码如下,其中onStatus方法中抛出的RemoteApiCallException为待处理目标异常,当前配置的doOnError可正常触发日志逻辑,但异常仍会抛出打印堆栈:

var monoVersionReply = webClient.post().uri(versionInfoApi)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .retrieve()
            .onStatus(HttpStatus::isError, response -> {
                if (response.statusCode().is4xxClientError()) {
                    logger.error("4xx client error: " + response.statusCode() + ", message:" + response);
                } else {
                    logger.error("server error: " + response.statusCode() + ", message:" + response);
                }
                return Mono.error(new RemoteApiCallException("service response code is not 200: " + response.statusCode()));
            })
            .bodyToMono(String.class)
            .onErrorMap(Predicate.not(RemoteApiCallException.class::isInstance), throwable -> new RemoteApiCallException(throwable.getMessage()))
            .doOnError(e -> logAllErrors(e));

monoVersionReply.subscribe(reply -> handleGetVersionResponse(reply));
根因分析

doOnError是响应式流的副作用操作,仅用于触发流异常时的附加动作(比如打日志、埋点),不会消费异常,异常仍会沿着流向下传递到订阅层。
当前调用subscribe方法时仅传入了正常响应的消费回调,没有指定异常消费逻辑,流中未被消费的异常会直接冒泡到响应式框架的全局异常处理器,最终触发完整堆栈打印。

修复方案

所有脱离Web请求链路的异步Mono/Flux订阅,必须在subscribe调用时显式传入异常处理回调,流中所有未被前置操作消费的异常都会进入该回调,不会再向外抛出触发堆栈打印。
修正后的代码如下:

var monoVersionReply = webClient.post().uri(versionInfoApi)
        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .retrieve()
        .onStatus(HttpStatus::isError, response -> {
            if (response.statusCode().is4xxClientError()) {
                logger.error("4xx client error: " + response.statusCode() + ", message:" + response);
            } else {
                logger.error("server error: " + response.statusCode() + ", message:" + response);
            }
            return Mono.error(new RemoteApiCallException("service response code is not 200: " + response.statusCode()));
        })
        .bodyToMono(String.class)
        .onErrorMap(Predicate.not(RemoteApiCallException.class::isInstance), throwable -> new RemoteApiCallException(throwable.getMessage()));

// 订阅时同时传入正常消费回调、异常处理回调
monoVersionReply.subscribe(
        reply -> handleGetVersionResponse(reply),
        e -> {
            // 统一异常处理逻辑:日志记录、告警埋点等,异常在此处被消费,不会继续向外抛出
            logAllErrors(e);
        }
);

可选优化

如果需要在异常发生时返回默认值、保证后续逻辑正常执行,可以在流中使用onErrorResume提前消费异常,无需等到订阅层处理:

var monoVersionReply = webClient.post().uri(versionInfoApi)
        // 省略前置请求配置
        .bodyToMono(String.class)
        .onErrorMap(Predicate.not(RemoteApiCallException.class::isInstance), throwable -> new RemoteApiCallException(throwable.getMessage()))
        .onErrorResume(e -> {
            logAllErrors(e);
            // 异常场景返回默认值,后续正常回调会收到该值
            return Mono.just("");
        });

内容的提问来源于stack exchange,提问作者Shehan Simen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:27:23