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

使用Spring WebClient exchange方法时如何抛出自定义异常

问题

我已实现一个复用方法,通过Spring WebClient的exchange方法返回HTTP调用的响应头、状态码与响应体。现在需要在方法内部捕获4xx和5xx错误并记录响应体,但无法在出现这类错误时返回/抛出自定义异常AutomationException。

当前的复用方法代码如下:

private Mono<ResponseEntity<Mono<String>>> exchangeGet(AutomationHttpRequest request) throws AutomationException {
    oLogger.debug("Invoking Exchange request for - {}", request.getName());
    return webClient
            .get()
            .uri(request.getUri())
            .headers(request.getHeaderList())
            .exchangeToMono(Mono::just)
            .map( response -> {

//                    Approach 1
//                    if(response.statusCode().is4xxClientError()){
//                        oLogger.error("Client side error for request {}, status {}", request.getName(), response.statusCode());
//                        oLogger.error("response body - ");
//                        response.toEntity(String.class).subscribe(error -> oLogger.error(String.valueOf(error.getBody())));
//                        throw new AutomationException("");
//                    }
//
//                    Approach 2
//                    if(response.statusCode().is4xxClientError()){
//                        oLogger.error("Client side error for request {}, status {}", request.getName(), response.statusCode());
//                        oLogger.error("response body - ");
//                        response.toEntity(String.class).subscribe(error -> oLogger.error(String.valueOf(error.getBody())));
//                        return Mono.error(new AutomationException(""));
//                    }

                HttpHeaders headers = response.headers().asHttpHeaders().entrySet().stream()
                        .filter(entry -> !entry.getKey().equalsIgnoreCase("Transfer-Encoding"))
                        .collect(HttpHeaders::new,
                                (httpHeaders, entry) -> httpHeaders.addAll(entry.getKey(), entry.getValue()), HttpHeaders::putAll);
                return ResponseEntity.status(response.statusCode())
                        .headers(headers).body(response.bodyToMono(String.class));
            });
}

注:AutomationException是自定义异常类。

两种尝试的方案都存在问题:

  • 方案1直接抛出异常时,编译器提示错误未处理;
  • 方案2返回Mono.error时,返回类型与方法返回类型不匹配,且因SonarLint规则限制无法使用泛型。

我希望实现的效果和使用retrieve方法的示例一致:

private Mono<String> get(AutomationHttpRequest request) throws AutomationException {
    oLogger.debug("Invoking GET request for - {}", request.getName());
    return webClient
            .get()
            .uri(request.getUri())
            .headers(request.getHeaderList())
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, response -> {
                oLogger.error(LOG_CLIENT_ERROR, request.getName(), response.statusCode());
                response.toEntity(String.class).subscribe(error -> oLogger.error(String.valueOf(error.getBody())));
                return Mono.error(new AutomationException(CLIENT_EXCEPTION));
            }).onStatus(HttpStatus::is5xxServerError, response -> {
                oLogger.error(LOG_SERVER_ERROR, request.getName(), response.statusCode());
                response.toEntity(String.class).subscribe(error -> oLogger.error(String.valueOf(error.getBody())));
                return Mono.error(new AutomationException(SERVER_EXCEPTION));
            })
            .bodyToMono(String.class);
}

该示例通过retrieve方法可分别处理4xx和5xx错误,记录错误状态与响应体,并抛出自定义异常。

需求:

  1. 使用exchange方法实现上述相同效果;
  2. 不在消费类中记录异常,统一在复用方法内处理;
  3. 标准化错误场景下抛出的异常;
  4. 需要通过exchange获取响应头和响应体,若有更优实现方式也请告知。

解决方案

核心问题分析

问题根源在于map操作符是同步处理,无法直接返回Mono.error;同时直接抛出异常会被视为同步异常,不符合Reactor的异步错误处理模型。另外,原方法返回Mono<ResponseEntity<Mono<String>>>的嵌套结构不合理,会增加消费端的处理复杂度,建议调整为Mono<ResponseEntity<String>>。

正确实现代码

private Mono<ResponseEntity<String>> exchangeGet(AutomationHttpRequest request) {
    oLogger.debug("Invoking Exchange request for - {}", request.getName());
    return webClient
            .get()
            .uri(request.getUri())
            .headers(request.getHeaderList())
            .exchangeToMono(response -> {
                // 处理错误状态码
                if (response.statusCode().is4xxClientError()) {
                    return handleErrorResponse(response, request.getName(), "客户端异常");
                }
                if (response.statusCode().is5xxServerError()) {
                    return handleErrorResponse(response, request.getName(), "服务端异常");
                }
                // 处理正常响应
                HttpHeaders headers = response.headers().asHttpHeaders().entrySet().stream()
                        .filter(entry -> !entry.getKey().equalsIgnoreCase("Transfer-Encoding"))
                        .collect(HttpHeaders::new,
                                (httpHeaders, entry) -> httpHeaders.addAll(entry.getKey(), entry.getValue()),
                                HttpHeaders::putAll);
                return response.bodyToMono(String.class)
                        .map(body -> ResponseEntity.status(response.statusCode())
                                .headers(headers)
                                .body(body));
            });
}

// 抽取错误处理逻辑,复用代码
private Mono<ResponseEntity<String>> handleErrorResponse(ClientResponse response, String requestName, String errorMsg) {
    oLogger.error("{} 请求错误,状态码: {}", requestName, response.statusCode());
    return response.bodyToMono(String.class)
            .doOnNext(errorBody -> oLogger.error("错误响应体: {}", errorBody))
            .flatMap(errorBody -> Mono.error(new AutomationException(errorMsg)));
}

关键改进点说明

  1. 调整返回类型:将Mono<ResponseEntity<Mono<String>>>改为Mono<ResponseEntity<String>>,避免嵌套Mono,简化消费端处理,同时符合Reactor的异步流模型。
  2. 使用exchangeToMono内部处理:直接在exchangeToMono的lambda中处理响应,而不是先Mono::just再map,这样可以直接返回Mono类型的结果,包括错误信号。
  3. 错误处理异步化:通过bodyToMono异步获取错误响应体,使用doOnNext记录日志,再通过flatMap转换为Mono.error抛出AutomationException,符合Reactor的错误处理规范。
  4. 抽取复用逻辑:把错误处理代码抽成单独的handleErrorResponse方法,提高代码可读性和复用性。
  5. 移除不必要的throws AutomationException:Reactor中异常是通过Mono.error传递的,不需要在方法签名上声明受检异常(如果AutomationException是受检异常,建议改为非受检异常,或者在handleErrorResponse中包装)。

额外优化建议

  • 如果AutomationException是受检异常,需要在handleErrorResponse中包装为RuntimeException,或者调整异常类型为非受检,避免破坏Reactor的异步模型。
  • 日志记录使用SLF4J的参数化日志,避免字符串拼接。
  • 可以考虑统一封装错误处理逻辑为WebClient的过滤器(ExchangeFilterFunction),进一步复用错误处理逻辑,无需在每个请求方法中重复编写。

内容的提问来源于stack exchange,提问作者Boudhayan Dev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 04:45:37