Рубрики
Без рубрики

Обработка ошибок для отдыха с пружиной

Обработка исключений для API REST – проиллюстрируйте рекомендуемый подход Spring 3.2+, а также более ранние решения .

Автор оригинала: Eugen Paraschiv.

1. Обзор

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

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

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

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

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

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

Дальнейшее чтение:

Пользовательская обработка сообщений об ошибках для REST API

Руководство по валидаторам Spring Data REST

Пользовательская проверка Spring MVC

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

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

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

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

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

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

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

3. Решение 2: решатель HandlerExceptionResolver

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

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

3.1. ExceptionHandlerExceptionResolver

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

3.2. DefaultHandlerExceptionResolver

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

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

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

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

3.3. ResponseStatusExceptionResolver

Этот распознаватель также был представлен весной 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. SimpleMappingExceptionResolver и AnnotationMethodHandlerExceptionResolver

SimpleMappingExceptionResolver существует уже довольно давно. Он исходит из более старой модели Spring MVC и не очень актуален для службы ОТДЫХА. Мы в основном используем его для сопоставления имен классов исключений для просмотра имен.

AnnotationMethodHandlerExceptionResolver был введен в Spring 3.0 для обработки исключений через @ExceptionHandler аннотацию, но был устаревшим ExceptionHandlerExceptionResolver с весны 3.2.

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

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

В идеале мы хотели бы иметь возможность выводить либо 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.

Однако у него есть ограничения: Он взаимодействует с низкоуровневым Httpservletresponse и вписывается в старую модель 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 handleConflict(
      RuntimeException ex, WebRequest request) {
        String bodyOfResponse = "This should be application specific";
        return handleExceptionInternal(ex, bodyOfResponse, 
          new HttpHeaders(), HttpStatus.CONFLICT, request);
    }
}

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

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

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

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

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

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

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

5. Решение 4: Исключение ResponseStatusException (Весна 5 и выше)

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

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

@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 s локально.

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

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

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

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

6.1. MVC — Пользовательская страница ошибок

Во-первых, давайте рассмотрим стиль MVC решения и посмотрим, как настроить страницу ошибок для отказа в доступе.

Конфигурация XML:


       
    ... 
    

И конфигурация Java:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

Когда пользователи пытаются получить доступ к ресурсу, не имея достаточных полномочий, они будут перенаправлены на “/my-error-page” .

6.2. Пользовательский AccessDeniedHandler

Далее давайте посмотрим, как написать наш пользовательский AccessDeniedHandler :

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) 
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

А теперь давайте настроим его с помощью XML-конфигурации :


     
    ...
    

0r использование конфигурации Java:

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
}

Обратите внимание , как в нашем CustomAccessDeniedHandler мы можем настроить ответ по своему усмотрению, перенаправив или отобразив пользовательское сообщение об ошибке.

6.3. Безопасность на уровне отдыха и методов

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

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

@ControllerAdvice
public class RestResponseEntityExceptionHandler 
  extends ResponseEntityExceptionHandler {

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

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

Пружинный ботинок обеспечивает Контроллер ошибок реализация для разумной обработки ошибок.

В двух словах, он обслуживает резервную страницу ошибок для браузеров (она же страница ошибок Whitelabel) и ответ JSON для спокойных, не 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-stack trace : со значением всегда ; включает трассировку стека как в HTML, так и в ответе по умолчанию JSON
  • server.error.include-message: начиная с версии 2.3, Spring Boot скрывает поле message в ответе, чтобы избежать утечки конфиденциальной информации; мы можем использовать это свойство со значением always , чтобы включить его

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

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

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

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

        //...

        return errorAttributes;
    }
}

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

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

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

@Component
public class MyErrorController extends BasicErrorController {

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

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity> xmlError(HttpServletRequest request) {
        
    // ...

    }
}

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

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

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

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

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