Статьи

Обработка ошибок REST API с помощью Spring Boot

Вступление

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

  • Даже если мы решим обработать все исключения с помощью пользовательского интерфейса @ExceptionHandler, некоторые исключения все же удастся избежать этого обработчика, и ErrorControllerони будут отвечать за обработку исключений. Это @ExceptionHandlerпротив ErrorControllerдвойственности, безусловно, может быть улучшено.
  • Иногда представление ошибок по умолчанию выглядит немного грязно:
{
  "timestamp": "2018-09-23T15:05:32.681+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
        "NotBlank.dto.name",
        "NotBlank.name",
        "NotBlank.java.lang.String",
        "NotBlank"
      ],
      "arguments": [
        {
          "codes": [
            "dto.name",
            "name"
          ],
          "arguments": null,
          "defaultMessage": "name",
          "code": "name"
        }
      ],
      "defaultMessage": "{name.not_blank}",
      "objectName": "dto",
      "field": "name",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotBlank"
    }
  ],
  "message": "Validation failed for object='dto'. Error count: 1",
  "path": "/"
}

Конечно, мы можем улучшить это, зарегистрировав пользовательскую ErrorAttributesреализацию, но было бы желательно более разумное и взвешенное (и все еще легко настраиваемое) представление по умолчанию.

  • Для ошибок проверки можно легко представить некоторые аргументы из ограничения в интерполированное сообщение. Например, минимальное количество для intзначения может быть передано в интерполированное сообщение с использованием {}синтаксиса заполнителя. Но то же самое не верно для других исключений:
public class UserAlreadyExistsException extends RuntimeException {

    // How can I expose this value to the interpolated message?
    private final String username;

    // constructors and getters and setters
}

  • Было бы хорошо, если бы у нас была встроенная поддержка кодов ошибок уровня приложения для всех исключений. Иногда, просто используя соответствующий код состояния HTTP,  мы не можем выяснить, что именно пошло не так. Например, что если две совершенно разные ошибки в одной и той же конечной точке имеют одинаковый код состояния?

Есть куда расти

Это errors-spring-boot-starterпопытка обеспечить гибкий, последовательный и самоуверенный подход для обработки всевозможных исключений. Созданные на основе механизма обработки исключений Spring Boot, errors-spring-boot-starterпредложения:

  • Последовательный подход для обработки всех исключений — не имеет значения, является ли это ошибкой валидации / привязки, пользовательской специфичной для домена ошибкой или даже ошибкой, связанной с Spring. Все они будут обработаны реализацией WebErrorHandler (больше не ErrorController vs @ExceptionHandler)
  • Встроенная поддержка специфичных для приложения кодов ошибок, опять же, для всех возможных ошибок.
  • Простая интерполяция сообщений об ошибках с использованием MessageSource.
  • Настраиваемое представление ошибок HTTP.
  • Предоставление аргументов из исключений сообщениям об ошибках.

Представление ошибок по умолчанию

По умолчанию ошибки будут результатом JSON со следующей схемой:

// For each error, there is a code/messsge combination in the errors array
{
  "errors": [
    {
      "code": "first_error_code",
      "message": "1st error message"
    }
  ]
}

Чтобы настроить это представление, просто зарегистрируйте HttpErrorAttributesAdapterреализацию как  Spring Bean .

Согласованный подход к обработке ошибок

Все исключения будут обработаны WebErrorHandlerреализацией. По умолчанию этот стартер будет обращаться к нескольким встроенным WebErrorHandlerметодам для обработки следующих конкретных исключений:

  • Реализация для обработки всех исключений валидации / связывания.
  • Реализация для обработки пользовательских исключений, аннотированных с помощью @ExceptionMapping.
  • Реализация для обработки особых исключений Spring MVC.
  • И если Spring Security находится на пути к классам, реализация для обработки особых исключений Spring Security.

Вы можете легко зарегистрировать свой собственный обработчик исключений, реализовав WebErrorHandlerреализацию и зарегистрировав ее как Spring Bean .

Поддержка встроенного кода ошибки

Хотя использование соответствующих кодов состояния HTTP является рекомендуемым подходом в API-интерфейсах RESTful, иногда нам требуется дополнительная информация, чтобы выяснить, что именно пошло не так. Вот тут и появляются коды ошибок. Код ошибки можно рассматривать как машиночитаемое описание ошибки. Каждое исключение может быть сопоставлено как минимум с одним кодом ошибки.

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

  • Коды ошибок валидации будут извлечены из message атрибута соответствующей аннотации ограничения, например @NotBlank(message = "name.required").
  • errorCodeАтрибут для исключения аннотированного с @ExceptionMappingвыглядит следующим образом :
@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {}

  • Вот код из пользовательских реализаций WebErrorHandler:
public class ExistedUserHandler implements WebErrorHandler {

    @Override
    public boolean canHandle(Throwable exception) {
        return exception instanceof UserAlreadyExistsException;
    }

    @Override
    public HandledException handle(Throwable exception) {   
        return new HandledException("user.already_exists", BAD_REQUEST, null);
    }
}

Разоблачение аргументов

Как и в Spring Boot, вы можете передавать аргументы проверки из аннотации ограничения, например @Min(value = 18, message = "age.min"), в сообщение, подлежащее интерполяции :

age.min = The minimum age is {0}!

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

user.already_exists=Another user with the '{0}' username already exists

Ты можешь написать:

@ExceptionMapping(statusCode = BAD_REQUEST, errorCode = "user.already_exists")
public class UserAlreadyExistsException extends RuntimeException {
    @ExposeAsArg(0) private final String username;

    // constructor
}

Заключение

В этой статье мы перечислили несколько возможных улучшений по сравнению с подходом обработки исключений в Spring Boot, представив errors-spring-boot-starterстартовый пакет. Для получения дополнительной информации об этом стартере, проверьте этот  репозиторий GitHub .