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

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

1. обзор

Эта статья проиллюстрируетhow to implement Exception Handling with Spring for a REST API. Мы также получим небольшой исторический обзор и посмотрим, какие новые возможности были введены в различных версиях.

Before Spring 3.2, the two main approaches to handling exceptions in a Spring MVC application were: HandlerExceptionResolver or the @ExceptionHandler annotation. У обоих есть очевидные недостатки.

Since 3.2 we’ve had the @ControllerAdvice annotation, чтобы устранить ограничения двух предыдущих решений и продвигать единую обработку исключений во всем приложении.

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

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

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

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

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

Реализуйте глобальный обработчик исключений для REST API с помощью Spring.

Read more

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

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

Read more

Spring MVC Custom Validation

Узнайте, как создать пользовательскую аннотацию проверки и использовать ее в Spring MVC.

Read more

2. Решение 1. Уровень контроллера@ExceptionHandler

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

public class FooController{

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

У этого подхода есть серьезный недостаток -the @ExceptionHandler annotated method is only active for that particular Controller, а не глобально для всего приложения. Конечно, добавление этого к каждому контроллеру делает его не очень подходящим для общего механизма обработки исключений.

Мы можем обойти это ограничение, указавall Controllers extend a Base Controller class - однако это может быть проблемой для приложений, где по какой-либо причине это невозможно. Например, контроллеры могут уже расширяться из другого базового класса, который может находиться в другом фляге или не может быть напрямую изменен, или сами могут не быть непосредственно модифицируемыми.

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

3. Решение 2 -The HandlerExceptionResolver

Второе решение - определитьHandlerExceptionResolver - это разрешит любое исключение, созданное приложением. Это также позволит нам реализоватьuniform exception handling mechanism в нашем REST API.

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

3.1. ExceptionHandlerExceptionResolverс

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

3.2. DefaultHandlerExceptionResolverс

Этот преобразователь был представлен в Spring 3.0 и по умолчанию включен вDispatcherServlet. Он используется для разрешения стандартных исключений Spring для соответствующих кодов состояния HTTP, а именно: ошибка клиента -4xx и ошибка сервера - коды состояния5xx. Here’s the full list исключений Spring, которые он обрабатывает, и способ их сопоставления с кодами состояния.

Хотя он правильно устанавливает код состояния ответа, одинlimitation is that it doesn’t set anything to the body of the Response. А для 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. SimpleMappingExceptionResolver и AnnotationMethodHandlerExceptionResolver

SimpleMappingExceptionResolver существует уже довольно давно - он исходит из более старой модели Spring MVC и равенnot very relevant for a REST Service. Мы в основном используем его для отображения имен классов исключений для просмотра имен.

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

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

КомбинацияDefaultHandlerExceptionResolver иResponseStatusExceptionResolver имеет большое значение для обеспечения хорошего механизма обработки ошибок для службы Spring RESTful. Обратной стороной является, как упоминалось ранее,no control over the body of the response.

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

Одно это оправдывает созданиеa new, custom exception resolver:

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

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

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

Другая важная деталь реализации - этоwe return a ModelAndView – this is the body of the response, и он позволит нам установить для него все, что необходимо.

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

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

Spring 3.2 поддерживаетa global @ExceptionHandler with the @ControllerAdvice annotation. Это позволяет создать механизм, который отличается от старой модели 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 позволяет намconsolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.

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

  • Полный контроль над телом ответа, а также кодом состояния

  • Сопоставление нескольких исключений одному и тому же методу, которые будут обрабатываться вместе, и

  • Он хорошо использует новый ответ RESTfulResposeEntity __

Здесь нужно иметь в виду, чтоmatch the exceptions declared with @ExceptionHandler with the exception used as the argument of the method. Если они не совпадают, компилятор не будет жаловаться - без причины, и Spring тоже не будет жаловаться.

Однако, когда исключение действительно возникает во время выполнения,the exception resolving mechanism will fail with:

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

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

Spring 5 представил классResponseStatusException. Мы можем создать его экземпляр, предоставивHttpStatus и, возможно,reason иcause:

@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?

  • Отлично подходит для создания прототипов: мы можем довольно быстро реализовать базовое решение

  • Один тип, несколько кодов состояния: один тип исключения может привести к нескольким различным ответам. This reduces tight coupling compared to the @ExceptionHandlerс

  • Нам не нужно будет создавать столько пользовательских классов исключений

  • More control over exception handling, поскольку исключения могут быть созданы программно

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

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

  • Дублирование кода: мы можем копировать код на несколько контроллеров

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

For example, we can implement a @ControllerAdvice globally, but also ResponseStatusExceptions locally. Однако нам нужно быть осторожными: если одно и то же исключение может быть обработано несколькими способами, мы можем заметить некоторое неожиданное поведение. Возможное соглашение - обрабатывать одно конкретное исключение всегда одним способом.

Для получения дополнительных сведений и дополнительных примеров см. Нашtutorial on 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 Configuration:


    
    ...
    

Или используя конфигурацию 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. REST и безопасность на уровне методов

Наконец, давайте посмотрим, как управлять безопасностью на уровне метода@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. Поддержка Spring Boot

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: со значениемalways , он включает трассировку стека как в HTML, так и в ответ по умолчанию JSON

Помимо этих свойствwe can provide our own view-resolver mapping for /error, overriding the Whitelabel Page.

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

@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {

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

        //...

        return errorAttributes;
    }
}

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

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

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

@Component
public class MyErrorController extends BasicErrorController {

    public MyErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

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

    // ...

    }
}

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

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

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

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