使用 RouterFunction 在 WebFlux 中处理错误
Error handling in WebFlux with RouterFunction
我无法让我的反应式代码以通用方式处理错误。理想的方法是在一个可重用的组件中,我可以将其作为依赖项添加到其他项目中。
以前我们都是@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);
}
}
此外,处理程序 class:
@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();
}
我也尝试过使用 AbstractErrorWebExceptionHandler
作为 Baeldung 中的示例。似乎也不起作用:
@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 进行全局错误处理?
@ControllerAdvice
仅适用于带注释的编程模型。要使用功能端点提供 ControllerAdvice
等功能,您可以利用 HandlerFilterFunction。来自参考:
Routes mapped by a router function can be filtered by calling RouterFunction.filter(HandlerFilterFunction), where HandlerFilterFunction is essentially a function that takes a ServerRequest and HandlerFunction, and returns a ServerResponse. The handler function parameter represents the next element in the chain: this is typically the HandlerFunction that is routed to, but can also be another FilterFunction if multiple filters are applied. With annotations, similar functionality can be achieved using @ControllerAdvice and/or a 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();
});
}
对我来说,我编写了一个 AppException 并将其扔到应用程序(Rest 控制器)中我认为应该是 "error" 响应的任何位置。
AppException:我的特定异常,它可以包含任何你想要处理、显示、return错误。
public class AppException extends RuntimeException {
int code;
HttpStatus status = HttpStatus.OK;
...
}
然后我定义 (a) 全局 ControllerAdvice,它负责过滤掉那些 AppExceptions。
这是我的示例,我可以取出我在 Rest Controller 中抛出的 AppException,然后 return 它作为 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 的回答。
我无法让我的反应式代码以通用方式处理错误。理想的方法是在一个可重用的组件中,我可以将其作为依赖项添加到其他项目中。
以前我们都是@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);
}
}
此外,处理程序 class:
@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();
}
我也尝试过使用 AbstractErrorWebExceptionHandler
作为 Baeldung 中的示例。似乎也不起作用:
@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 进行全局错误处理?
@ControllerAdvice
仅适用于带注释的编程模型。要使用功能端点提供 ControllerAdvice
等功能,您可以利用 HandlerFilterFunction。来自参考:
Routes mapped by a router function can be filtered by calling RouterFunction.filter(HandlerFilterFunction), where HandlerFilterFunction is essentially a function that takes a ServerRequest and HandlerFunction, and returns a ServerResponse. The handler function parameter represents the next element in the chain: this is typically the HandlerFunction that is routed to, but can also be another FilterFunction if multiple filters are applied. With annotations, similar functionality can be achieved using @ControllerAdvice and/or a 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();
});
}
对我来说,我编写了一个 AppException 并将其扔到应用程序(Rest 控制器)中我认为应该是 "error" 响应的任何位置。
AppException:我的特定异常,它可以包含任何你想要处理、显示、return错误。
public class AppException extends RuntimeException { int code; HttpStatus status = HttpStatus.OK; ... }
然后我定义 (a) 全局 ControllerAdvice,它负责过滤掉那些 AppExceptions。
这是我的示例,我可以取出我在 Rest Controller 中抛出的 AppException,然后 return 它作为 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 的回答。