【问题标题】:Error handling in WebFlux with RouterFunction使用 RouterFunction 处理 WebFlux 中的错误
【发布时间】:2026-01-25 15:25:01
【问题描述】:

我无法让我的反应式代码以通用方式处理错误。理想的方式是使用可重用组件,我可以将其作为依赖项添加到其他项目中。

过去,我们使用@RestControllerAdvise 来处理个性化的@ExceptionHandler 函数。作为参考,我的代码:

@Configuration
public class VesselRouter {

    @Bean
    public RouterFunction<ServerResponse> route(VesselHandler handler) {
        return RouterFunctions.route(GET("/vessels/{imoNumber}").and(accept(APPLICATION_JSON)), handler::getVesselByImo)
                .andRoute(GET("/vessels").and(accept(APPLICATION_JSON)), handler::getVessels);
    }
}

另外,处理程序类:

@Component
@AllArgsConstructor
public class VesselHandler {
    private VesselsService vesselsService;

    public Mono<ServerResponse> getVesselByImo(ServerRequest request) {
        String imoNumber = request.pathVariable("imoNumber");
        Mono<VesselResponse> response = this.vesselsService.getByImoNumber(imoNumber);
        return response.hasElement().flatMap(vessel -> {
                if (vessel) {
                    return ServerResponse.ok()
                            .contentType(APPLICATION_JSON)
                            .body(response, VesselResponse.class);
                } else {
                    throw new DataNotFoundException("The data you seek is not here.");
                }
            }
        );

    }

    public Mono<ServerResponse> getVessels(ServerRequest request) {
        return this.vesselsService.getAllVessels();
    }
}
/**
 * Exception class to be thrown when data not found for the requested resource
 */
public class DataNotFoundException extends RuntimeException {

    public DataNotFoundException(String e) {
        super(e);
    }
}

在我们的公共库中:

@ControllerAdvice(assignableTypes={VesselHandler.class})
// FIXME: referencing class here is not good, it will create circular dependency when moved to it's own jar
@Slf4j
public class ExceptionHandlers {

    @ExceptionHandler(value = DataNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleDataNotFoundException(DataNotFoundException dataNotFoundException,
                                                                ServletWebRequest servletWebRequest) {
        //habdling expcetions code here
    }
}

还有异常处理程序:

@ControllerAdvice
@Slf4j
public class ExceptionHandlers {

    @ExceptionHandler(value = DataNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleDataNotFoundException(DataNotFoundException dataNotFoundException,
                                                                ServletWebRequest servletWebRequest) {
        //habdling expcetions code here
    }
}

我在spring documentation 中读到,这是它应该工作的方式,但我的单元测试似乎并没有在异常处理程序附近进行:

@Test
    public void findByImoNoData() {
        when(vesselsService.getByImoNumber("1234567")).thenReturn(Mono.empty());
        webTestClient.get().uri("/vessels/1234567")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isNotFound();
    }

我也尝试使用AbstractErrorWebExceptionHandlerBaeldung 中的示例一样。似乎也不起作用:

@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, applicationContext);
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(
            ErrorAttributes errorAttributes) {

        return RouterFunctions.route(
                RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(
            ServerRequest request) {

        Map<String, Object> errorPropertiesMap = getErrorAttributes(request, false);

        return ServerResponse.status(HttpStatus.BAD_REQUEST)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(errorPropertiesMap));
    }
}

那么,如何在不使用 @RestController 的情况下使用 WebFlux 进行全局错误处理?

【问题讨论】:

    标签: spring-webflux


    【解决方案1】:

    @ControllerAdvice 仅适用于带注释的编程模型。要为ControllerAdvice 之类的功能提供功能端点,您可以利用HandlerFilterFunction。来自参考:

    路由函数映射的路由可以通过调用RouterFunction.filter(HandlerFilterFunction)进行过滤,其中HandlerFilterFunction本质上是一个接受ServerRequest和HandlerFunction,并返回ServerResponse的函数。 handler 函数参数表示链中的下一个元素:这通常是路由到的 HandlerFunction,但如果应用了多个过滤器,也可以是另一个 FilterFunction。通过注解,可以使用 @ControllerAdvice 和/或 ServletFilter 实现类似的功能。

    @Bean
    RouterFunction<ServerResponse> route() {
        return RouterFunctions
                .route(GET("/foo"), request -> Mono.error(new DataNotFoundException()))
                .andRoute(GET("/bar"), request -> Mono.error(new DataNotFoundException()))
                .filter(dataNotFoundToBadRequest());
    }
    
    private HandlerFilterFunction<ServerResponse, ServerResponse> dataNotFoundToBadRequest() {
        return (request, next) -> next.handle(request)
                .onErrorResume(DataNotFoundException.class, e -> ServerResponse.badRequest().build());
    }
    

    或者,您可以使用 WebFilter 来完成同样的事情:

    @Bean
    RouterFunction<ServerResponse> route() {
        return RouterFunctions
                .route(GET("/foo"), request -> Mono.error(new DataNotFoundException()))
                .andRoute(GET("/bar"), request -> Mono.error(new DataNotFoundException()));
    }
    
    @Bean
    WebFilter dataNotFoundToBadRequest() {
        return (exchange, next) -> next.filter(exchange)
                .onErrorResume(DataNotFoundException.class, e -> {
                    ServerHttpResponse response = exchange.getResponse();
                    response.setStatusCode(HttpStatus.BAD_REQUEST);
                    return response.setComplete();
                });
    }
    

    【讨论】:

      【解决方案2】:

      对我来说,我创建了一个 AppException 并将它扔到应用程序(Rest 控制器)中我认为应该是“错误”响应的任何地方。

      • AppException:我的具体异常,它可以包含任何你想处理、显示、返回错误的东西。

        public class AppException extends RuntimeException {
            int code;
            HttpStatus status = HttpStatus.OK;
            ...
        }
        

      然后我定义 (a) 全局 ControllerAdvice 负责过滤掉那些 AppExceptions。

      这是我的示例,我可以取出我在 Rest Controller 中抛出的 AppException,然后将其作为 ReponseEntity 返回,主体为“ErrorResponse”POJO。

      public class ErrorResponse {
      
          boolean error = true;
          int code;
          String message;
      }
      
      @ControllerAdvice
      public class GlobalExceptionHandlingControllerAdvice {
      
          @ExceptionHandler(AppException.class)
          public ResponseEntity handleAppException(AppException ex) {
              return ResponseEntity.ok(new ErrorResponse(ex.getCode(), ex.getMessage()));
          }
      }
      

      在 webflux 中,错误可能会通过 return Mono.error() 作为 Rob Winch 的答案抛出。

      【讨论】: