spring-webflux 中处理错误的正确方法是啥

Posted

技术标签:

【中文标题】spring-webflux 中处理错误的正确方法是啥【英文标题】:what is the right way to handle errors in spring-webfluxspring-webflux 中处理错误的正确方法是什么 【发布时间】:2017-09-20 09:27:32 【问题描述】:

我一直在使用 spring-webflux 进行一些研究,我想了解使用路由器功能处理错误的正确方法应该是什么。

我创建了一个小项目来测试几个场景,我希望获得有关它的反馈,看看其他人在做什么。

到目前为止,我所做的是。

给出如下路由函数:

@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());
    

这是完整的示例代码库:

https://github.com/LearningByExample/reactive-ms-example

【问题讨论】:

【参考方案1】:

如果您认为路由器函数不是处理异常的正确位置,则抛出 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 异常非常接近业务逻辑,在这种情况下应该放在业务逻辑附近。而且由于在微服务中你不应该有太多的业务逻辑和异常,它也不应该让你的代码变得混乱......(当然,这一切都取决于)。

【讨论】:

【参考方案2】:

Spring 5 提供了一个WebHandler,在 JavaDoc 中有这样一行:

使用 HttpWebHandlerAdapter 使 WebHandler 适应 HttpHandler。 WebHttpHandlerBuilder 提供了一种方便的方式来执行此操作,同时还可以选择配置一个或多个过滤器和/或异常处理程序。

目前,官方文档建议我们应该在启动任何服务器之前将路由器函数包装到一个 HttpHandler 中:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(routerFunction);

借助WebHttpHandlerBuilder,我们可以配置自定义异常处理程序:

HttpHandler httpHandler = WebHttpHandlerBuilder.webHandler(toHttpHandler(routerFunction))
  .prependExceptionHandler((serverWebExchange, exception) -> 

      /* custom handling goes here */
      return null;

  ).build();

【讨论】:

【参考方案3】:

为什么不通过从处理函数抛出异常并实现您自己的 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。

【讨论】:

终于让它工作了,在none boot应用中,手动注册,见here @Hantsy 您还可以考虑直接在处理函数中处理异常,例如 onErrorResume(...)。我实际上发现这是最佳实践 atm :)【参考方案4】:

将您的异常映射到 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\""

【讨论】:

【参考方案5】:

您可以使用自定义响应数据和响应代码编写全局异常处理程序,如下所示。代码在 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))
  

【讨论】:

这是一个很好的解决方案,通过扩展 DefaultErrorWebExceptionHandler 而不是 ErrorWebExceptionHandler 做得更好。【参考方案6】:

我目前正在做的只是为我的 WebExceptionHandler 提供一个 bean:

@Bean
@Order(0)
public WebExceptionHandler responseStatusExceptionHandler() 
    return new MyWebExceptionHandler();

比自己创建HttpHandler 的优势在于,如果我提供自己的ServerCodecConfigurer 或使用SpringSecurity,我可以更好地与WebFluxConfigurer 集成。

【讨论】:

以上是关于spring-webflux 中处理错误的正确方法是啥的主要内容,如果未能解决你的问题,请参考以下文章

关于 Spring-WebFlux 的一些想法

Spring-WebFlux使用,一文带你从0开始学明白Spring-WebFlux,学明白响应式编程

如何在spring-webflux中获取当前请求的上下文

Spring-webflux 过滤器获取请求正文

使用 redux-promise 处理 redux 错误的正确方法

将 spring-webflux 微服务切换到 http/2 (netty)