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

如何用StepVerifier测试Spring Webflux的onErrorResume()及异常处理优化

问题解答

一、使用StepVerifier测试onErrorResume逻辑

测试思路为:先模拟WebClient调用抛出指定类型的异常,再通过StepVerifier订阅方法返回的Mono,验证异常的类型、属性是否符合预期。
示例测试代码如下:

import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;

// 测试普通异常被包装为WebFaultException的场景
@Test
void getInfo_WhenGeneralExceptionThrown_ShouldWrapToWebFaultException() {
    // 省略WebClient mock逻辑,最终让bodyToMono返回普通运行时异常
    when(responseSpec.bodyToMono(POJO.class)).thenReturn(Mono.error(new RuntimeException("下游调用失败")));

    StepVerifier.create(yourService.getInfo(1))
            // 验证异常属性符合预期
            .expectErrorMatches(ex -> ex instanceof WebFaultException faultEx
                    && faultEx.getReason().equals("Error on API Call")
                    && faultEx.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR
                    && faultEx.getMessage().equals("下游调用失败"))
            .verify();
}

// 测试原生WebFaultException直接透传的场景
@Test
void getInfo_WhenWebFaultExceptionThrown_ShouldPropagateDirectly() {
    WebFaultException originalEx = new WebFaultException("参数错误", "客户端异常", HttpStatus.BAD_REQUEST);
    when(responseSpec.bodyToMono(POJO.class)).thenReturn(Mono.error(originalEx));

    StepVerifier.create(yourService.getInfo(1))
            .expectErrorMatches(ex -> ex == originalEx) // 验证原异常没有被二次包装
            .verify();
}

二、控制器层异常处理优化方案

  • 全局异常统一处理:推荐使用@RestControllerAdvice统一处理所有接口抛出的异常,无需在每个服务/接口方法中重复编写异常封装逻辑,降低代码冗余。示例代码如下:
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 处理系统抛出的非自定义异常
    @ExceptionHandler(value = {RuntimeException.class, WebClientResponseException.class})
    public Mono<ResponseEntity<ErrorResp>> handleGeneralException(Exception ex) {
        WebFaultException faultEx = new WebFaultException(ex.getMessage(), "接口调用异常", HttpStatus.INTERNAL_SERVER_ERROR);
        return Mono.just(ResponseEntity.status(faultEx.getStatus()).body(new ErrorResp(faultEx.getCode(), faultEx.getReason())));
    }

    // 处理自定义WebFaultException
    @ExceptionHandler(value = WebFaultException.class)
    public Mono<ResponseEntity<ErrorResp>> handleWebFaultException(WebFaultException ex) {
        return Mono.just(ResponseEntity.status(ex.getStatus()).body(new ErrorResp(ex.getCode(), ex.getReason())));
    }
}
  • switchIfEmpty的使用场景:当下游接口返回200状态码但响应体为空时,bodyToMono会返回Mono.empty(),不会触发onError流程,此时可以用switchIfEmpty主动抛出异常,补全空响应场景的处理。
  • 细粒度状态码处理:可以在retrieve后通过onStatus方法提前处理特定HTTP状态码的异常,比统一在onErrorResume中处理逻辑更清晰。改造后的业务代码示例如下:
public Mono<SomeType> getInfo(Integer id) {
        return webClient
                .get()
                .uri(uriBuilder -> uriBuilder.path())
                .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .retrieve()
                // 提前处理4xx类错误
                .onStatus(HttpStatus::is4xxClientError, resp -> 
                    Mono.error(new WebFaultException("请求参数异常", "客户端错误", HttpStatus.BAD_REQUEST))
                )
                // 提前处理5xx类错误
                .onStatus(HttpStatus::is5xxServerError, resp -> 
                    Mono.error(new WebFaultException("下游服务异常", "服务端错误", HttpStatus.BAD_GATEWAY))
                )
                .bodyToMono(POJO.class)
                // 处理响应体为空的场景
                .switchIfEmpty(Mono.error(new WebFaultException("查询结果不存在", "空响应", HttpStatus.NOT_FOUND)))
                .onErrorResume(ex -> {
                    if (ex instanceof WebFaultException)
                        return Mono.error(ex);
                    return Mono.error(new WebFaultException(ex.getMessage(), "接口调用异常", HttpStatus.INTERNAL_SERVER_ERROR));
                });
}
  • 异常分层处理:服务层仅做业务逻辑相关的异常转换,HTTP响应格式的封装统一放到全局异常处理器中实现,职责拆分更清晰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 06:15:03