Статьи
Утилиты 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: "один-ко-многим", чтение данных мы создадим дочернюю сущность «Город» и будем возвращать информацию о двух связанных между собой сущностях.


Облако тэгов

Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

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

×

devmark.ru