spring-webflux 中处理错误的正确方法是什么
what is the right way to handle errors in spring-webflux
我一直在使用 spring-webflux 进行一些研究,我想了解使用 Router Functions 处理错误的正确方法应该是什么。
我创建了一个小项目来测试几个场景,我希望获得有关它的反馈,并看看其他人在做什么。
到目前为止我所做的是。
提供以下路由功能:
@Component
public class HelloRouter {
@Bean
RouterFunction<?> helloRouterFunction() {
HelloHandler handler = new HelloHandler();
ErrorHandler error = new ErrorHandler();
return nest(path("/hello"),
nest(accept(APPLICATION_JSON),
route(GET("/"), handler::defaultHello)
.andRoute(POST("/"), handler::postHello)
.andRoute(GET("/{name}"), handler::getHello)
)).andOther(route(RequestPredicates.all(), error::notFound));
}
}
我已经在我的处理程序上这样做了
class HelloHandler {
private ErrorHandler error;
private static final String DEFAULT_VALUE = "world";
HelloHandler() {
error = new ErrorHandler();
}
private Mono<ServerResponse> getResponse(String value) {
if (value.equals("")) {
return Mono.error(new InvalidParametersException("bad parameters"));
}
return ServerResponse.ok().body(Mono.just(new HelloResponse(value)), HelloResponse.class);
}
Mono<ServerResponse> defaultHello(ServerRequest request) {
return getResponse(DEFAULT_VALUE);
}
Mono<ServerResponse> getHello(ServerRequest request) {
return getResponse(request.pathVariable("name"));
}
Mono<ServerResponse> postHello(ServerRequest request) {
return request.bodyToMono(HelloRequest.class).flatMap(helloRequest -> getResponse(helloRequest.getName()))
.onErrorResume(error::badRequest);
}
}
他们是我的错误处理程序:
class ErrorHandler {
private static Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
private static BiFunction<HttpStatus,String,Mono<ServerResponse>> response =
(status,value)-> ServerResponse.status(status).body(Mono.just(new ErrorResponse(value)),
ErrorResponse.class);
Mono<ServerResponse> notFound(ServerRequest request){
return response.apply(HttpStatus.NOT_FOUND, "not found");
}
Mono<ServerResponse> badRequest(Throwable error){
logger.error("error raised", error);
return response.apply(HttpStatus.BAD_REQUEST, error.getMessage());
}
}
这是完整的示例存储库:
Spring 5 提供了一个 WebHandler,在 JavaDoc 中,有一行:
Use HttpWebHandlerAdapter to adapt a WebHandler to an HttpHandler. The WebHttpHandlerBuilder provides a convenient way to do that while also optionally configuring one or more filters and/or exception handlers.
目前,官方文档建议我们应该在启动任何服务器之前将路由器功能包装到 HttpHandler 中:
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction);
在WebHttpHandlerBuilder的帮助下,我们可以配置自定义异常处理程序:
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(toHttpHandler(routerFunction))
.prependExceptionHandler((serverWebExchange, exception) -> {
/* custom handling goes here */
return null;
}).build();
为什么不通过从处理程序函数中抛出异常并实现您自己的 WebExceptionHandler 来捕获所有异常来采用老式的方式:
@Component
class ExceptionHandler : WebExceptionHandler {
override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
/* Handle different exceptions here */
when(ex!!) {
is NoSuchElementException -> exchange!!.response.statusCode = HttpStatus.NOT_FOUND
is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
}
/* Do common thing like logging etc... */
return Mono.empty()
}
}
上面的例子是在 Kotlin 中,因为我只是从我目前正在进行的项目中复制粘贴它,并且因为原始问题没有被标记为 java。
我目前正在做的只是提供一个 bean 我的 WebExceptionHandler :
@Bean
@Order(0)
public WebExceptionHandler responseStatusExceptionHandler() {
return new MyWebExceptionHandler();
}
与自己创建 HttpHandler
相比的优势在于,如果我提供自己的 ServerCodecConfigurer
或使用 SpringSecurity
[=,我可以更好地与 WebFluxConfigurer
集成15=]
如果您认为路由器函数不是处理异常的正确位置,您可以抛出 HTTP 异常,这将导致正确的 HTTP 错误代码。
对于 Spring-Boot(也是 webflux),这是:
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
.
.
.
new ResponseStatusException(HttpStatus.NOT_FOUND, "Collection not found");})
spring 证券 AccessDeniedException 也将得到正确处理(403/401 响应代码)。
如果你有一个微服务,并且想为其使用 REST,这可能是一个不错的选择,因为那些 http 异常非常接近业务逻辑,在这种情况下应该放在业务逻辑附近。而且由于在微服务中你不应该有太多的业务逻辑和异常,它也不应该使你的代码混乱......(当然,这一切都取决于)。
将异常映射到 http 响应状态的快速方法是抛出 org.springframework.web.server.ResponseStatusException
/ 或创建自己的子类...
完全控制 http 响应状态 + spring 将添加一个响应正文,并可选择添加 reason
。
在 Kotlin 中它可能看起来像
一样简单
@Component
class MyHandler(private val myRepository: MyRepository) {
fun getById(req: ServerRequest) = req.pathVariable("id").toMono()
.map { id -> uuidFromString(id) } // throws ResponseStatusException
.flatMap { id -> noteRepository.findById(id) }
.flatMap { entity -> ok().json().body(entity.toMono()) }
.switchIfEmpty(notFound().build()) // produces 404 if not found
}
fun uuidFromString(id: String?) = try { UUID.fromString(id) } catch (e: Throwable) { throw BadRequestStatusException(e.localizedMessage) }
class BadRequestStatusException(reason: String) : ResponseStatusException(HttpStatus.BAD_REQUEST, reason)
响应正文:
{
"timestamp": 1529138182607,
"path": "/api/notes/f7b.491bc-5c86-4fe6-9ad7-111",
"status": 400,
"error": "Bad Request",
"message": "For input string: \"f7b.491bc\""
}
您可以使用自定义响应数据和响应代码编写全局异常处理程序,如下所示。代码在 Kotlin 中。但是您可以轻松地将其转换为 java:
@Component
@Order(-2)
class GlobalWebExceptionHandler(
private val objectMapper: ObjectMapper
) : ErrorWebExceptionHandler {
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
val response = when (ex) {
// buildIOExceptionMessage should build relevant exception message as a serialisable object
is IOException -> buildIOExceptionMessage(ex)
else -> buildExceptionMessage(ex)
}
// Or you can also set them inside while conditions
exchange.response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON
exchange.response.statusCode = HttpStatus.valueOf(response.status)
val bytes = objectMapper.writeValueAsBytes(response)
val buffer = exchange.response.bufferFactory().wrap(bytes)
return exchange.response.writeWith(Mono.just(buffer))
}
}
我一直在使用 spring-webflux 进行一些研究,我想了解使用 Router Functions 处理错误的正确方法应该是什么。
我创建了一个小项目来测试几个场景,我希望获得有关它的反馈,并看看其他人在做什么。
到目前为止我所做的是。
提供以下路由功能:
@Component
public class HelloRouter {
@Bean
RouterFunction<?> helloRouterFunction() {
HelloHandler handler = new HelloHandler();
ErrorHandler error = new ErrorHandler();
return nest(path("/hello"),
nest(accept(APPLICATION_JSON),
route(GET("/"), handler::defaultHello)
.andRoute(POST("/"), handler::postHello)
.andRoute(GET("/{name}"), handler::getHello)
)).andOther(route(RequestPredicates.all(), error::notFound));
}
}
我已经在我的处理程序上这样做了
class HelloHandler {
private ErrorHandler error;
private static final String DEFAULT_VALUE = "world";
HelloHandler() {
error = new ErrorHandler();
}
private Mono<ServerResponse> getResponse(String value) {
if (value.equals("")) {
return Mono.error(new InvalidParametersException("bad parameters"));
}
return ServerResponse.ok().body(Mono.just(new HelloResponse(value)), HelloResponse.class);
}
Mono<ServerResponse> defaultHello(ServerRequest request) {
return getResponse(DEFAULT_VALUE);
}
Mono<ServerResponse> getHello(ServerRequest request) {
return getResponse(request.pathVariable("name"));
}
Mono<ServerResponse> postHello(ServerRequest request) {
return request.bodyToMono(HelloRequest.class).flatMap(helloRequest -> getResponse(helloRequest.getName()))
.onErrorResume(error::badRequest);
}
}
他们是我的错误处理程序:
class ErrorHandler {
private static Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
private static BiFunction<HttpStatus,String,Mono<ServerResponse>> response =
(status,value)-> ServerResponse.status(status).body(Mono.just(new ErrorResponse(value)),
ErrorResponse.class);
Mono<ServerResponse> notFound(ServerRequest request){
return response.apply(HttpStatus.NOT_FOUND, "not found");
}
Mono<ServerResponse> badRequest(Throwable error){
logger.error("error raised", error);
return response.apply(HttpStatus.BAD_REQUEST, error.getMessage());
}
}
这是完整的示例存储库:
Spring 5 提供了一个 WebHandler,在 JavaDoc 中,有一行:
Use HttpWebHandlerAdapter to adapt a WebHandler to an HttpHandler. The WebHttpHandlerBuilder provides a convenient way to do that while also optionally configuring one or more filters and/or exception handlers.
目前,官方文档建议我们应该在启动任何服务器之前将路由器功能包装到 HttpHandler 中:
HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction);
在WebHttpHandlerBuilder的帮助下,我们可以配置自定义异常处理程序:
HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(toHttpHandler(routerFunction))
.prependExceptionHandler((serverWebExchange, exception) -> {
/* custom handling goes here */
return null;
}).build();
为什么不通过从处理程序函数中抛出异常并实现您自己的 WebExceptionHandler 来捕获所有异常来采用老式的方式:
@Component
class ExceptionHandler : WebExceptionHandler {
override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
/* Handle different exceptions here */
when(ex!!) {
is NoSuchElementException -> exchange!!.response.statusCode = HttpStatus.NOT_FOUND
is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
}
/* Do common thing like logging etc... */
return Mono.empty()
}
}
上面的例子是在 Kotlin 中,因为我只是从我目前正在进行的项目中复制粘贴它,并且因为原始问题没有被标记为 java。
我目前正在做的只是提供一个 bean 我的 WebExceptionHandler :
@Bean
@Order(0)
public WebExceptionHandler responseStatusExceptionHandler() {
return new MyWebExceptionHandler();
}
与自己创建 HttpHandler
相比的优势在于,如果我提供自己的 ServerCodecConfigurer
或使用 SpringSecurity
[=,我可以更好地与 WebFluxConfigurer
集成15=]
如果您认为路由器函数不是处理异常的正确位置,您可以抛出 HTTP 异常,这将导致正确的 HTTP 错误代码。 对于 Spring-Boot(也是 webflux),这是:
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
.
.
.
new ResponseStatusException(HttpStatus.NOT_FOUND, "Collection not found");})
spring 证券 AccessDeniedException 也将得到正确处理(403/401 响应代码)。
如果你有一个微服务,并且想为其使用 REST,这可能是一个不错的选择,因为那些 http 异常非常接近业务逻辑,在这种情况下应该放在业务逻辑附近。而且由于在微服务中你不应该有太多的业务逻辑和异常,它也不应该使你的代码混乱......(当然,这一切都取决于)。
将异常映射到 http 响应状态的快速方法是抛出 org.springframework.web.server.ResponseStatusException
/ 或创建自己的子类...
完全控制 http 响应状态 + spring 将添加一个响应正文,并可选择添加 reason
。
在 Kotlin 中它可能看起来像
一样简单@Component
class MyHandler(private val myRepository: MyRepository) {
fun getById(req: ServerRequest) = req.pathVariable("id").toMono()
.map { id -> uuidFromString(id) } // throws ResponseStatusException
.flatMap { id -> noteRepository.findById(id) }
.flatMap { entity -> ok().json().body(entity.toMono()) }
.switchIfEmpty(notFound().build()) // produces 404 if not found
}
fun uuidFromString(id: String?) = try { UUID.fromString(id) } catch (e: Throwable) { throw BadRequestStatusException(e.localizedMessage) }
class BadRequestStatusException(reason: String) : ResponseStatusException(HttpStatus.BAD_REQUEST, reason)
响应正文:
{
"timestamp": 1529138182607,
"path": "/api/notes/f7b.491bc-5c86-4fe6-9ad7-111",
"status": 400,
"error": "Bad Request",
"message": "For input string: \"f7b.491bc\""
}
您可以使用自定义响应数据和响应代码编写全局异常处理程序,如下所示。代码在 Kotlin 中。但是您可以轻松地将其转换为 java:
@Component
@Order(-2)
class GlobalWebExceptionHandler(
private val objectMapper: ObjectMapper
) : ErrorWebExceptionHandler {
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
val response = when (ex) {
// buildIOExceptionMessage should build relevant exception message as a serialisable object
is IOException -> buildIOExceptionMessage(ex)
else -> buildExceptionMessage(ex)
}
// Or you can also set them inside while conditions
exchange.response.headers.contentType = MediaType.APPLICATION_PROBLEM_JSON
exchange.response.statusCode = HttpStatus.valueOf(response.status)
val bytes = objectMapper.writeValueAsBytes(response)
val buffer = exchange.response.bufferFactory().wrap(bytes)
return exchange.response.writeWith(Mono.just(buffer))
}
}