Статьи
Утилиты Telegram YouTube Отзывы

Spring Data JPA, REST и Kotlin: обработка ошибок

Видеогайд Исходники

19 марта 2023

Тэги: gradle, json, Kotlin, rest, Spring Boot, YouTube, руководство.

Содержание

  1. Кастомный формат ответа при ошибке
  2. Базовый класс для всех исключений
  3. Глобальный обработчик ошибок
  4. Пример кастомного исключения

В предыдущей статье Spring Data JPA, REST и Kotlin: создание, обновление, удаление мы научились изменять данные в базе с помощью Spring Data JPA. При обновлении и удалении мы сначала проверяем, что запись существует. И если она не найдена – кидаем стандартное исключение. Обеспечивается это поведение через элвис-оператор:

var existingCountry = countryRepository.findByIdOrNull(id)
    ?: throw RuntimeException("Country not found")

Если возникает данная ошибка, то клиент в ответ получит json примерно следующего содержания:

{
    "timestamp": "2023-03-18T20:29:28.822+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/countries/10"
}

Такой формат идёт в Spring Boot «из коробки». Здесь есть какая-то второстепенная информация, но при этом нет самого главного – причины ошибки. Кроме того, у каждого типа ошибки может быть своя дополнительная информация, которая облегчает диагностику проблемы. Конечно, с точки зрения безопасности не стоит возвращать детали внутренней реализации в ответе на запрос. Для этого существует логирование на сервере. Но возвращать хотя бы осмысленный код ошибки не помешает.

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

Кастомный формат ответа при ошибке

Создадим новый data class под названием ApiError.

data class ApiError(
    val errorCode: String, // country.not.found
    val description: String,
)

Как видите, он содержит код ошибки и её описание.

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

Мне нравится такой текстовый формат, в котором все слова записаны строчными буквами, а вместо пробелов используются точки. Но это дело вкуса.

Базовый класс для всех исключений

Теперь можем создать базовый абстрактный класс BaseException для всех кастомных исключений в нашем приложении.

abstract class BaseException(
    val httpStatus: HttpStatus,
    val apiError: ApiError,
): RuntimeException(apiError.description)

В него мы добавляем созданный нами ApiError, а также поле стандартного типа HttpStatus, т.к. у каждой ошибки в rest API должен быть свой статус HTTP. Например, если запись не найдена – принято возвращать статус 404.

Базовое исключение наследуем от RuntimeException, чтобы его не нужно было явно прописывать в сигнатурах метода. Это больше имеет смысла для взаимодействия с Java, т.к. в Kotlin все исключения unchecked. В конструктор RuntimeException передаем description – это уточняющее описание ошибки.

Глобальный обработчик ошибок

Теперь нужно создать глобальный обработчик ошибок ErrorHandler.

@ControllerAdvice
class ErrorHandler : ResponseEntityExceptionHandler() {
    @ExceptionHandler(BaseException::class)
    fun handleBaseException(ex: BaseException): ResponseEntity<ApiError> {
        return ResponseEntity(ex.apiError, ex.httpStatus)
    }
}

На этот обработчик вешаем аннотацию @ControllerAdvice. Также требуется унаследовать его от ResponseEntityExceptionHandler.

Затем в обработчике мы можем объявлять методы, которые обрабатывают специфические исключения благодаря аннотации @ExceptionHandler. Если мы укажем BaseException в этой аннотации, то метод будет обрабатывать исключения данного типа и его наследников в любом месте нашего приложения.

В качестве результата метод возвращает ResponseEntity, где первый параметр – это тело ответа, а второй – httpStatus. Всю эту информацию мы берём из нашего исключения.

Пример кастомного исключения

Теперь создадим исключение бизнес-логики CountryNotFoundException.

class CountryNotFoundException(countryId: Int) : BaseException(
    HttpStatus.NOT_FOUND,
    ApiError(
        errorCode = "country.not.found",
        description = "Country not found with id=$countryId"
    )
)

Наследуем его от BaseException. В качестве http-кода указываем HttpStatus.NOT_FOUND (тот самый 404), а в ApiError указываем текстовый код ошибки и словесное описание с указанием id страны, которая не найдена.

Теперь в сервисном слое в CountryServiceImpl заменим все фрагменты кода с поиском страны и RuntimeException на созданное нами исключение:

val existingCountry = countryRepository.findByIdOrNull(id)
    ?: throw CountryNotFoundException(id)

Как видите, кастомное исключение использовать удобнее, потому что даже текст ошибки писать не нужно – это инкапсулировано в самом исключении. А значимые параметры ошибки передаются через конструктор.

Теперь, если страна не найдена, ответ будет выглядеть так:

{
    "errorCode": "country.not.found",
    "description": "Country not found with id=10"
}

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

В следующей статье Spring Data JPA, REST и Kotlin: "один-ко-многим", чтение данных мы создадим дочернюю сущность «Город» и будем возвращать информацию о двух связанных между собой сущностях.



Комментарии

Добавить комментарий

×

devmark.ru