Перейти к основному содержимому

Обработка ошибок в Spring WebFlux

· 5 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим различные стратегии, доступные для обработки ошибок в проекте Spring WebFlux , и рассмотрим практический пример.

Мы также укажем, где может быть выгодно использовать одну стратегию по сравнению с другой, и предоставим ссылку на полный исходный код в конце.

2. Настройка примера

Настройка Maven такая же, как и в нашей предыдущей статье , в которой представлено введение в Spring WebFlux.

В нашем примере мы будем использовать конечную точку RESTful, которая принимает имя пользователя в качестве параметра запроса и в результате возвращает «Hello username» .

Во-первых, давайте создадим функцию маршрутизатора, которая направляет запрос /hello в метод с именем handleRequest в переданном обработчике:

@Bean
public RouterFunction<ServerResponse> routeRequest(Handler handler) {
return RouterFunctions.route(RequestPredicates.GET("/hello")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
handler::handleRequest);
}

Далее мы определим метод handleRequest() , который вызывает метод sayHello() и находит способ включить/возвратить его результат в тело ServerResponse :

public Mono<ServerResponse> handleRequest(ServerRequest request) {
return
//...
sayHello(request)
//...
}

Наконец, метод sayHello() — это простой служебный метод, который объединяет строку «Hello» и имя пользователя:

private Mono<String> sayHello(ServerRequest request) {
//...
return Mono.just("Hello, " + request.queryParam("name").get());
//...
}

Пока имя пользователя присутствует как часть нашего запроса, например, если конечная точка вызывается как « /hello?username=Tonni », эта конечная точка всегда будет работать правильно.

Однако если мы вызовем ту же конечную точку без указания имени пользователя, например, « /hello », будет выдано исключение.

Ниже мы рассмотрим, где и как мы можем реорганизовать наш код для обработки этого исключения в WebFlux.

3. Обработка ошибок на функциональном уровне

В API Mono и Flux встроены два ключевых оператора для обработки ошибок на функциональном уровне.

Давайте кратко рассмотрим их и их использование.

3.1. Обработка ошибок с помощью onErrorReturn

Мы можем использовать onErrorReturn() для возврата статического значения по умолчанию при возникновении ошибки:

public Mono<ServerResponse> handleRequest(ServerRequest request) {
return sayHello(request)
.onErrorReturn("Hello Stranger")
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s));
}

Здесь мы возвращаем статическое сообщение «Hello Stranger» всякий раз, когда ошибочная функция конкатенации sayHello() выдает исключение.

3.2. Обработка ошибок с помощью onErrorResume

Есть три способа, которыми мы можем использовать onErrorResume для обработки ошибок:

  • Вычислить динамическое резервное значение
  • Выполнить альтернативный путь с резервным методом
  • Перехватывать, обертывать и повторно выдавать ошибку, например, в качестве пользовательского бизнес-исключения.

Давайте посмотрим, как мы можем вычислить значение:

public Mono<ServerResponse> handleRequest(ServerRequest request) {
return sayHello(request)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s))
.onErrorResume(e -> Mono.just("Error " + e.getMessage())
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s)));
}

Здесь мы возвращаем строку, состоящую из динамически полученного сообщения об ошибке, добавляемого к строке «Ошибка» каждый раз , когда функция sayHello() выдает исключение.

Далее, давайте вызовем резервный метод при возникновении ошибки :

public Mono<ServerResponse> handleRequest(ServerRequest request) {
return sayHello(request)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s))
.onErrorResume(e -> sayHelloFallback()
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s)));
}

Здесь мы вызываем альтернативный метод sayHelloFallback() всякий раз , когда sayHello() генерирует исключение.

Последний вариант использования onErrorResume()перехватить, обернуть и повторно выдать ошибку , например, как NameRequiredException :

public Mono<ServerResponse> handleRequest(ServerRequest request) {
return ServerResponse.ok()
.body(sayHello(request)
.onErrorResume(e -> Mono.error(new NameRequiredException(
HttpStatus.BAD_REQUEST,
"username is required", e))), String.class);
}

Здесь мы выбрасываем пользовательское исключение с сообщением «требуется имя пользователя» всякий раз, когда функция sayHello() выдает исключение.

4. Обработка ошибок на глобальном уровне

До сих пор все примеры, которые мы представили, касались обработки ошибок на функциональном уровне.

Однако мы можем решить обрабатывать наши ошибки WebFlux на глобальном уровне. Для этого нам нужно сделать всего два шага:

  • Настройте глобальные атрибуты ответа на ошибку
  • Реализуйте глобальный обработчик ошибок

Исключение, которое выдает наш обработчик, будет автоматически преобразовано в статус HTTP и тело ошибки JSON.

Чтобы настроить их, мы можем просто расширить класс DefaultErrorAttributes и переопределить его метод getErrorAttributes() :

public class GlobalErrorAttributes extends DefaultErrorAttributes{

@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(
request, options);
map.put("status", HttpStatus.BAD_REQUEST);
map.put("message", "username is required");
return map;
}

}

Здесь мы хотим, чтобы статус: BAD_REQUEST и сообщение «требуется имя пользователя» возвращались как часть атрибутов ошибки при возникновении исключения.

Далее давайте реализуем глобальный обработчик ошибок.

Для этого Spring предоставляет удобный класс AbstractErrorWebExceptionHandler , который мы можем расширить и реализовать для обработки глобальных ошибок:

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

// constructors

@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,
ErrorAttributeOptions.defaults());

return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
}

В этом примере мы устанавливаем порядок нашего глобального обработчика ошибок на -2. Это делается для того, чтобы дать ему более высокий приоритет, чем DefaultErrorWebExceptionHandler , который зарегистрирован в @Order(-1) .

Объект errorAttributes будет точной копией объекта, который мы передаем в конструкторе обработчика веб-исключений. В идеале это должен быть наш настраиваемый класс атрибутов ошибок.

Затем мы четко заявляем, что хотим направить все запросы на обработку ошибок в метод renderErrorResponse() .

Наконец, мы получаем атрибуты ошибок и вставляем их в тело ответа сервера.

Затем создается ответ JSON с подробными сведениями об ошибке, статусом HTTP и сообщением об исключении для машин-клиентов. Для клиентов браузера у него есть обработчик ошибок «белой метки», который отображает те же данные в формате HTML. Это, конечно, можно настроить.

5. Вывод

В этой статье мы рассмотрели различные стратегии, доступные для обработки ошибок в проекте Spring WebFlux, и указали, где может быть выгодно использовать одну стратегию по сравнению с другой.

Как и было обещано, полный исходный код, сопровождающий статью, доступен на GitHub .