Обработка исключений в контроллерах Spring Boot

Статья из группы Java Developer
СОДЕРЖАНИЕ ЦИКЛА СТАТЕЙ И снова здравствуйте! Пришло время стряхнуть пыль с клавиатуры. Создаем spring-boot проект. Из зависимостей мавена нам нужны:

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <java.version>1.8</java.version>
</properties>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.0.RELEASE</version>
    <relativePath/><!-- lookup parent from repository -->
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Прежде чем читать дальше, создайте структуру проекта: Обработка исключений в контроллерах Spring Boot - 1 BusinessException и CustomException:

public class BusinessException extends Exception{
    public BusinessException(String message) {
        super(message);
    }
}

public class CustomException extends Exception{
    public CustomException(String message) {
        super(message);
    }
}
и класс Response

public class Response {

    private String message;

    public Response() {
    }

    public Response(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
А теперь, я сделаю финт ушами, и передам слово Алексею Кутепову, в своей статье Обработка исключений в контроллерах Spring он нам расскажет, как эти файлы наполнить правильным содержимым. Читайте не торопясь, все примеры аккуратно переписываете к себе в проект, запускайте и тестируйте в постмане. Если в статье Алексея, у вас вызвала вопросы следующая строчка: produces = APPLICATION_JSON_VALUE, то знайте, что к обработке исключений она отношения не имеет, она говорит, что по умолчанию все методы этого контроллера будут отдавать JSON. При необходимости в конкретном методе это значение можно переопределить на другой MediaType Если вы прочитали, идем дальше. В предлагаемой выше статье рассмотрены разные варианты обработчиков. Самый гибкий из них: @ControllerAdvice – он позволяет изменить как код, так и тело стандартного ответа при ошибке. Кроме того, он позволяет в одном методе обработать сразу несколько исключений. Но это еще не все, если вы прочитаете дальше, то получите улучшенный @ControllerAdvice совершенно бесплатно. Проведем подготовительные работы: хочу что бы в ответе выводились как кастомное так и стандартное сообщения об ошибке. Для этого внесем изменение в класс Response: добавим еще одно поле

private String debugMessage;
Создадим дополнительный конструктор:

public Response(String message, String debugMessage) {
    this.message = message;
    this.debugMessage = debugMessage;
}
и не забудьте, создать Getter и Setter для нового поля. Теперь к делу. Напишем еще один контроллер:

@RestController
public class Example7Controller {
    @GetMapping(value = "/testExtendsControllerAdvice")
    public ResponseEntity<?> testExtendsControllerAdvice(@RequestBody Response response) {
        return  ResponseEntity.ok(response);
    }
}
Протестируем в постман: На http://localhost:8080/testExtendsControllerAdvice отправим JSON

{
    "message": "message"
}
В ответ получим статус 200 и тело

{
    "message": "message",
    "debugMessage": null
}
Теперь пошлем заведомо не правильный JSON

{
    11"message": "message"
}
В ответ получим статус 400 (если забыли, что он значит, гляньте в интернете) и пустое тело ответа. Конечно же, никого это не устраивает, давайте с этим бороться. Ранее мы создавали @ControllerAdvice с нуля, но в Spring Boot существует заготовка – ResponseEntityExceptionHandler. В ней уже обработаны многие исключения, например: NoHandlerFoundException, HttpMessageNotReadableException, MethodArgumentNotValidException и другие. Данный класс занимается обработкой ошибок. У него куча методов, название которых построено по принципу handle + название исключения. Если мы хотим обработать какое-то базовое исключение, то наследуемся от этого класса и переопределяем нужный метод. Доработаем класс дефолтного эдвайса

@ControllerAdvice
public class DefaultAdvice extends ResponseEntityExceptionHandler {//унаследовались от обработчика-заготовки

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Response> handleException(BusinessException e) {
        Response response = new Response(e.getMessage());
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
//Небольшое отступление: В обработчике выше, обратите внимание на HttpStatus.OK, 
//он может быть и HttpStatus.BAD_REQUEST или другим, тут ограничений нет, 
//попробуйте поменять статусы и потестить этот обработчик


    @Override//переопределили метод родительского класса
    protected ResponseEntity<Object> handleHttpMessageNotReadable
            (HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        Response response = new Response("Не правильный JSON",ex.getMessage());
        return new ResponseEntity<>(response, status);
    }
}
Как вы заметили, был переопределен обработчик, отвечающий за HttpMessageNotReadableException. Это исключение возникает тогда, когда тело запроса, приходящего в метод контроллер, нечитаемое – например, некорректный JSON. За это исключение отвечает метод handleHttpMessageNotReadable(). Еще раз сделаем запрос с некорректным JSON: на http://localhost:8080/testExtendsControllerAdvice

{
    11"message": "message"
}
Получаем ответ с кодом 400 (Bad Request) и телом:

{
    "message": "Не правильный JSON",
    "debugMessage": "JSON parse error: Unexpected character ('1' (code 49)): was expecting double-quote to start field name; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('1' (code 49)): was expecting double-quote to start field name\n at [Source: (PushbackInputStream); line: 2, column: 6]"
}
Теперь ответ содержит не только корректный код, но и тело с информативными сообщениями. Проверим как работает с корректным JSON Запрос:

{
    "message": "message"
}
Получили ответ:

{
    "message": "message",
    "debugMessage": null
}
Если честно, мне не нравиться что в ответе есть поле со значением null, сейчас быстренько это исправим. Переходим в класс Response и ставим аннотацию над нужным полем

@JsonInclude(JsonInclude.Include.NON_NULL)
private String debugMessage;
Перезапускаем проект, делаем еще раз предыдущий запрос, в ответе получаем:

{
    "message": "message"
}
Благодаря аннотации @JsonInclude(JsonInclude.Include.NON_NULL) это поле будет включено в ответ только в том случае, если мы его зададим. @JsonInclude входит в библиотеку аннотаций Jackson, очень полезно знать, что она может. Вот две статьи на выбор: Джексон аннотации. Автор переводил, но не допереводил, гугл транслит отлично справиться. Валидация Необходимо дополнить эту тему таким понятием как валидация. Просто говоря, это проверка что объект это тот объект который мы ожидаем. Например: если мы в приложении "Телефонный справочник" должны проверять наличие телефонных номеров в БД, то прежде чем лезть в базу, логично проверять, а не ввел ли пользователь вместо цифр буквы. Три статьи по валидации, по возврастанию сложности: Валидация бинов в Spring Настройка валидации DTO в Spring Framework Валидация данных в Spring Boot С теорией, на сегодня закончили. Для тренировки предлагаю следующее задание: Необходимо реализовать приложение NightclubBouncer (Вышибала ночного клуба). Требования: 1) Приложение должно принимать на вход JSON и делать запись в базу данных. Пример JSON:

{
    "name": "Katy Perry"
    “status”: “super star”
}
И в теле ответа должна быть надпись: Welcome + name ! 2) У приложения должны быть реализованы методы: - вывод записи по id из БД в клиент (Postman). - удаление записи по полю: name. 3) Должен быть реализован маппинг из слоя dto в entity и обратно. 4) Приложение должно выбрасывать ошибку KickInTheAssException (ее нужно разработать самим) если поле status, во входящем JSON не равно: super star 5) Ошибка KickInTheAssException должна обрабатываться ControllerAdvice, и в теле ответа должно быть сообщение: «Don't let me see you here again!». Статус ответа должен быть 400. 6) Стандартная ошибка EntityNotFoundException, возникающая, например, если в клуб зашла только Кэти Пери и сохранилась в базу с id = 1, а вы вызвали метод «вывод записи по id» и захотели вывести запись с id = 2, которой нет в базе. Эту ошибку необходимо обработать переопределенным методом класса ResponseEntityExceptionHandler, каким именно - предстоит разобраться вам самим. Ответ должен иметь соответствующий статус. 7) Сделайте валидацию: простой вариант - поля JSON должны быть не null, по сложнее поле "name" должно состоять из двух слов латинского алфавита и они оба должны начинаться с заглавной буквы. Невалидные значения должны вызывать исключение, обработайте его любым способом, выведите соотвествующий код ошибки и сообщение об ошибке: No validate. И реализуйте это все без использования библиотеки Lombok, не включайте ее в зависимости проекта 😅
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Элен Уровень 41, Москва, Russian Federation
14 июля 2022
А почему нельзя Lombok?)))