如何用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
相关产品推荐
相关产品推荐

