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

Обработка ошибок для REST с помощью Spring

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

Задача: Сумма двух

Дано массив целых чисел и целая сумма. Нужно найти индексы двух чисел, сумма которых равна заданной ...

ANDROMEDA

1. Обзор

В этом руководстве показано, как реализовать обработку исключений с помощью Spring для REST API. Мы также получим небольшой исторический обзор и посмотрим, какие новые опции были представлены в разных версиях.

До Spring 3.2 двумя основными подходами к обработке исключений в приложении Spring MVC были HandlerExceptionResolver или аннотация @ExceptionHandler . У обоих есть явные недостатки.

Начиная с версии 3.2, у нас есть аннотация @ControllerAdvice для устранения ограничений предыдущих двух решений и продвижения единой обработки исключений во всем приложении.

Теперь в Spring 5 представлен класс ResponseStatusException — быстрый способ базовой обработки ошибок в наших REST API.

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

Наконец, мы увидим, что предлагает Spring Boot и как мы можем настроить его в соответствии с нашими потребностями.

2. Решение 1: @ExceptionHandler уровня контроллера ``

Первое решение работает на уровне @Controller . Мы определим метод для обработки исключений и аннотируем его с помощью @ExceptionHandler :

public class FooController{

//...
@ExceptionHandler({ CustomException1.class, CustomException2.class })
public void handleException() {
//
}
}

У этого подхода есть большой недостаток: аннотированный метод @ExceptionHandler активен только для этого конкретного контроллера , а не глобально для всего приложения. Конечно, добавление этого к каждому контроллеру делает его не очень подходящим для общего механизма обработки исключений.

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

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

Далее мы рассмотрим другой способ решения проблемы обработки исключений — глобальный и не включающий никаких изменений в существующих артефактах, таких как контроллеры.

3. Решение 2: HandlerExceptionResolver

Второе решение — определить HandlerExceptionResolver. Это разрешит любое исключение, созданное приложением. Это также позволит нам реализовать единый механизм обработки исключений в нашем REST API.

Прежде чем переходить к пользовательскому преобразователю, давайте рассмотрим существующие реализации.

3.1. ExceptionHandlerExceptionResolver

Этот преобразователь был представлен в Spring 3.1 и включен по умолчанию в DispatcherServlet . На самом деле это основной компонент того, как работает механизм @ExceptionHandler , представленный ранее.

3.2. DefaultHandlerExceptionResolver

Этот распознаватель был представлен в Spring 3.0 и включен по умолчанию в DispatcherServlet .

Он используется для разрешения стандартных исключений Spring для соответствующих кодов состояния HTTP , а именно для кодов состояния ошибки клиента 4xx и ошибки сервера 5xx . Вот полный список исключений Spring, которые он обрабатывает, и то, как они сопоставляются с кодами состояния.

Хотя он правильно устанавливает код состояния ответа, одним ограничением является то, что он ничего не устанавливает в теле ответа. А для REST API — кода состояния действительно недостаточно информации для представления клиенту — ответ также должен иметь тело, чтобы приложение могло предоставить дополнительную информацию о сбое.

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

3.3. ResponseStatusExceptionResolver

Этот распознаватель также был представлен в Spring 3.0 и включен по умолчанию в DispatcherServlet .

Его основная обязанность — использовать аннотацию @ResponseStatus , доступную для пользовательских исключений, и сопоставлять эти исключения с кодами состояния HTTP.

Такое пользовательское исключение может выглядеть так:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
public MyResourceNotFoundException() {
super();
}
public MyResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
public MyResourceNotFoundException(String message) {
super(message);
}
public MyResourceNotFoundException(Throwable cause) {
super(cause);
}
}

Так же, как и DefaultHandlerExceptionResolver , этот распознаватель ограничен в том, как он работает с телом ответа — он сопоставляет код состояния с ответом, но тело по-прежнему имеет значение null.

3.4. Пользовательский HandlerExceptionResolver

Комбинация DefaultHandlerExceptionResolver и ResponseStatusExceptionResolver имеет большое значение для обеспечения хорошего механизма обработки ошибок для Spring RESTful Service. Недостатком является, как упоминалось ранее, отсутствие контроля над телом ответа.

В идеале мы хотели бы иметь возможность выводить либо JSON, либо XML, в зависимости от того, какой формат запросил клиент (через заголовок Accept ).

Уже одно это оправдывает создание нового пользовательского преобразователя исключений :

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

@Override
protected ModelAndView doResolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
return handleIllegalArgument(
(IllegalArgumentException) ex, response, handler);
}
...
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "]
resulted in Exception", handlerException);
}
return null;
}

private ModelAndView
handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response)
throws IOException {
response.sendError(HttpServletResponse.SC_CONFLICT);
String accept = request.getHeader(HttpHeaders.ACCEPT);
...
return new ModelAndView();
}
}

Здесь следует отметить одну деталь: у нас есть доступ к самому запросу , поэтому мы можем рассмотреть значение заголовка Accept , отправленного клиентом.

Например, если клиент запрашивает application/json , то в случае ошибки мы хотели бы убедиться, что возвращаем тело ответа, закодированное с помощью application/json .

Другая важная деталь реализации заключается в том, что мы возвращаем ModelAndView — это тело ответа , и оно позволит нам установить в нем все, что необходимо.

Этот подход представляет собой согласованный и легко настраиваемый механизм обработки ошибок службы Spring REST.

Однако у него есть ограничения: он взаимодействует с низкоуровневым HtttpServletResponse и вписывается в старую модель MVC, использующую ModelAndView , так что еще есть возможности для улучшения.

4. Решение 3: @ControllerAdvice

Spring 3.2 поддерживает глобальный @ExceptionHandler с аннотацией @ControllerAdvice .

Это включает механизм, который отходит от старой модели MVC и использует ResponseEntity наряду с безопасностью типов и гибкостью @ExceptionHandler :

@ControllerAdvice
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {

@ExceptionHandler(value
= { IllegalArgumentException.class, IllegalStateException.class })
protected ResponseEntity<Object> handleConflict(
RuntimeException ex, WebRequest request) {
String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse,
new HttpHeaders(), HttpStatus.CONFLICT, request);
}
}

Аннотация @ControllerAdvice позволяет нам объединить наши многочисленные разрозненные @ExceptionHandler в единый глобальный компонент обработки ошибок.

Фактический механизм чрезвычайно прост, но также и очень гибок:

  • Это дает нам полный контроль над телом ответа, а также над кодом состояния.
  • Он обеспечивает сопоставление нескольких исключений с одним и тем же методом для совместной обработки.
  • Он хорошо использует новый ответ RESTful ResposeEntity . ``

Здесь следует помнить одну вещь : исключения, объявленные с помощью @ExceptionHandler , должны сопоставляться с исключением, используемым в качестве аргумента метода.

Если они не совпадают, компилятор не будет жаловаться — нет причин, по которым он должен — и Spring тоже не будет жаловаться.

Однако, когда исключение фактически генерируется во время выполнения, механизм разрешения исключений не работает с :

java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5. Решение 4: ResponseStatusException (Spring 5 и выше)

Spring 5 представил класс ResponseStatusException .

Мы можем создать его экземпляр, указав HttpStatus и, возможно, причину и причину :

@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
try {
Foo resourceById = RestPreconditions.checkFound(service.findOne(id));

eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
return resourceById;
}
catch (MyResourceNotFoundException exc) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Foo Not Found", exc);
}
}

Каковы преимущества использования ResponseStatusException ?

  • Отлично подходит для прототипирования: мы можем довольно быстро реализовать базовое решение.
  • Один тип, несколько кодов состояния: один тип исключения может привести к нескольким различным ответам. Это уменьшает тесную связь по сравнению с @ExceptionHandler .
  • Нам не нужно будет создавать столько настраиваемых классов исключений.
  • У нас больше контроля над обработкой исключений, поскольку исключения можно создавать программно.

А как насчет компромиссов?

  • Единого способа обработки исключений не существует: более сложно обеспечить соблюдение некоторых соглашений для всего приложения, чем @ControllerAdvice , который обеспечивает глобальный подход.
  • Дублирование кода: мы можем обнаружить, что дублируем код на нескольких контроллерах.

Также следует отметить, что в рамках одного приложения можно комбинировать разные подходы.

Например, мы можем реализовать @ControllerAdvice глобально, а также ResponseStatusException локально.

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

Дополнительные сведения и дополнительные примеры см. в нашем руководстве по ResponseStatusException .

6. Обработка отказа в доступе в Spring Security

Отказ в доступе возникает, когда аутентифицированный пользователь пытается получить доступ к ресурсам, для доступа к которым у него недостаточно прав.

6.1. REST и безопасность на уровне методов

Наконец, давайте посмотрим, как обрабатывать исключение «Отказано в доступе», создаваемое аннотациями безопасности на уровне метода — @PreAuthorize , @PostAuthorize и @Secure .

Конечно, мы будем использовать глобальный механизм обработки исключений, который обсуждался ранее, для обработки AccessDeniedException :

@ControllerAdvice
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {

@ExceptionHandler({ AccessDeniedException.class })
public ResponseEntity<Object> handleAccessDeniedException(
Exception ex, WebRequest request) {
return new ResponseEntity<Object>(
"Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
}

...
}

7. Поддержка весенней загрузки

Spring Boot предоставляет реализацию ErrorController для разумной обработки ошибок.

В двух словах, он служит резервной страницей ошибок для браузеров (также известной как страница ошибок Whitelabel) и ответом JSON для RESTful, не-HTML-запросов:

{
"timestamp": "2019-01-17T16:12:45.977+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Error processing the request!",
"path": "/my-endpoint-with-exceptions"
}

Как обычно, Spring Boot позволяет настраивать эти функции с помощью свойств:

  • server.error.whitelabel.enabled : может использоваться для отключения страницы ошибок Whitelabel и полагаться на контейнер сервлета для предоставления сообщения об ошибке HTML.
  • server.error.include-stacktrace : всегда значение; включает трассировку стека как в HTML, так и в ответ по умолчанию JSON
  • server.error.include-message: начиная с версии 2.3 Spring Boot скрывает поле сообщения в ответе, чтобы избежать утечки конфиденциальной информации; мы можем использовать это свойство со значением always , чтобы включить его

Помимо этих свойств, мы можем предоставить собственное сопоставление представления и разрешения для / error, переопределяющее страницу Whitelabel.

Мы также можем настроить атрибуты, которые мы хотим показать в ответе, включив в контекст bean-компонент ErrorAttributes . Мы можем расширить класс DefaultErrorAttributes , предоставляемый Spring Boot, чтобы упростить задачу:

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes =
super.getErrorAttributes(webRequest, options);
errorAttributes.put("locale", webRequest.getLocale()
.toString());
errorAttributes.remove("error");

//...

return errorAttributes;
}
}

Если мы хотим пойти дальше и определить (или переопределить), как приложение будет обрабатывать ошибки для определенного типа контента, мы можем зарегистрировать bean-компонент ErrorController .

Опять же, мы можем использовать BasicErrorController по умолчанию, предоставленный Spring Boot, чтобы помочь нам.

Например, представьте, что мы хотим настроить, как наше приложение обрабатывает ошибки, инициированные в конечных точках XML. Все, что нам нужно сделать, это определить общедоступный метод с помощью @RequestMapping и указать, что он создает тип носителя application/xml :

@Component
public class MyErrorController extends BasicErrorController {

public MyErrorController(
ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes, serverProperties.getError());
}

@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {

// ...

}
}

Примечание: здесь мы по-прежнему полагаемся на загрузочные свойства server.error.* , которые мы могли определить в нашем проекте и которые привязаны к bean- компоненту ServerProperties .

8. Заключение

В этой статье обсуждалось несколько способов реализации механизма обработки исключений для REST API в Spring, начиная с более старого механизма и продолжая поддержкой Spring 3.2 и переходя в версии 4.x и 5.x.

Как всегда, код, представленный в этой статье, доступен на GitHub .

Для кода, связанного с Spring Security, вы можете проверить модуль spring-security-rest .