как правильно обрабатывать ошибки в spring-webflux

Я провел некоторое исследование с использованием 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


person Juan Medina    schedule 23.04.2017    source источник


Ответы (6)


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();
person aietcn    schedule 19.06.2017

Если вы думаете, что функции маршрутизатора не подходят для обработки исключений, вы выбрасываете исключения 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-исключения довольно близки к бизнес-логике и в этом случае должны быть размещены рядом с бизнес-логикой. И поскольку в микросервисе у вас не должно быть много бизнес-логики и исключений, он не должен загромождать и ваш код ... (но, конечно, все зависит от обстоятельств).

person Frischling    schedule 14.02.2018

Почему бы не сделать это старомодным способом, выбрасывая исключения из функций-обработчиков и реализуя свой собственный 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.

person Alphaone    schedule 14.07.2017
comment
Наконец, заставьте его работать, ни в одном загрузочном приложении, зарегистрируйте его вручную, см. здесь - person Hantsy; 01.01.2018
comment
@Hantsy Вы также можете рассмотреть возможность обработки исключений непосредственно в функциях обработчика, например, с помощью onErrorResume (...). Я на самом деле считаю, что это лучший банкомат :) - person Alphaone; 07.02.2018

Быстрый способ сопоставить ваши исключения со статусом HTTP-ответа - это бросить org.springframework.web.server.ResponseStatusException / или создать свои собственные подклассы ...

Полный контроль над статусом ответа http + spring добавит тело ответа с возможностью добавления reason.

В Котлине это могло выглядеть так просто, как

@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\""
}
person Hartmut    schedule 17.06.2018

Вы можете написать глобальный обработчик исключений с пользовательскими данными ответа и кодом ответа следующим образом. Код находится на Котлине. Но вы можете легко преобразовать его в 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))
  }
}
person Akhil Bojedla    schedule 07.10.2019
comment
Это отличное решение, которое стало еще лучше за счет расширения DefaultErrorWebExceptionHandler вместо ErrorWebExceptionHandler. - person GhostBytes; 28.06.2021

В настоящее время я просто предоставляю bean-компонент my WebExceptionHandler:

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

Преимущество самостоятельного создания HttpHandler в том, что у меня лучшая интеграция с WebFluxConfigurer, если я предоставлю свой собственный ServerCodecConfigurer, например, или использую SpringSecurity

person adrien le roy    schedule 24.07.2017