Обработка исключений в Spring REST приложении: @RestControllerAdvice и @ExceptionHandler

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

Со структурой базы данных моего воображаемого музыкального магазина из этого учебного проекта можно ознакомиться в прошлых статьях:

Отношения One To Many в проекте Java Spring (Hibernate)

Составной первичный ключ в JPA/ Hibernate

Для начала я создам новый package и несколько классов для новых, уникальных исключений, которые могут произойти в моём приложении. Например, клиент может попробовать запросить ресурс, которого нет в базе. Для этого мне нужно NoSuchMusicEndpointException.

package com.winylka.Exceptions;

public class NoSuchMusicEndpointException extends RuntimeException {

    public NoSuchMusicEndpointException() {
        super();
    }

    public NoSuchMusicEndpointException(String message) {
        super(message);
    }
}

Я также сразу предусмотрел сценарий, когда клиент попытается отменить уже выполненный или отправленный заказ. Наверно, такие проблемы принято решать, просто не предоставляя в таких заказах ссылку для отмены, но вдруг там что-то пойдёт не так? В общем, в учебном проекте я решил сделать как можно больше, поэтому создал ещё и WrongStatusException на всякий случай. В нём кроме сообщения об ошибке также хранится информация о статусе: о том, который у заказа на данный момент, и о том, какой клиент пытается ему задать:

package com.winylka.Exceptions;
import com.winylka.data.entities.OrderStatus;

public class WrongStatusException extends RuntimeException {

    private OrderStatus currentStatus;
    private OrderStatus newStatus;

    public WrongStatusException() {
        super();
    }

    public WrongStatusException(String message) {
        super(message);
    }

    public OrderStatus getCurrentStatus() {
        return currentStatus;
    }

    public void setCurrentStatus(OrderStatus currentStatus) {
        this.currentStatus = currentStatus;
    }

    public OrderStatus getNewStatus() {
        return newStatus;
    }

    public void setNewStatus(OrderStatus newStatus) {
        this.newStatus = newStatus;
    }
}

Теперь можем создать обработчика исключений, который будет с ними самостоятельно справляться без нашего участия. Сначала я пытался построить приложение без этого класса, но быстро погряз во множестве исключений, которые перекидывались из сервиса в контроллер, и я просто не знал, где их обрабатывать, да и вообще, стоит ли их обрабатывать?

Класс ApiExceptionHandler является наследником ResponseEntityExceptionHandler, и его нужно аннотировать @RestControllerAdvice (или просто @ControllerAdvice, если контроллеры у вас не рестные).

Для каждого исключения, которое нужно перехватить и обработать, создаём свой метод handle с аннотацией @ExceptionHandler. В скобках указываем нужный класс:

@ExceptionHandler(WrongStatusException.class)

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

    @ExceptionHandler(NoSuchMusicEndpointException.class)
    public void handle(NoSuchMusicEndpointException e) {
        System.out.println("HEY, something is wrong");
    }

Проблема в том, что в таком случае сервер даст нам ответ 200 OK, мол, всё прошло хорошо. Даже если в ответе не будет альбома, артиста или релиза, как клиент ожидал. а будет пустота. А это не есть хорошо — клиент остаётся не в курсе, что что-то пошло не так. Нужно ему об этом сообщить. Для этого используем ResponseEntity.

    @ExceptionHandler(NoSuchMusicEndpointException.class)
    public ResponseEntity<String> handle(NoSuchMusicEndpointException e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
    }

Здесь мы возвращаем сообщение из исключения в виде строки и код NOT_FOUND, который в точности соответствует произошедшей ситуации: по данному адресу ничего нету. Куда делось?

Но я выбрал вариант посложнее — так клиент получает не просто строку, а полноценный объект в виде JSON, который может содержать не только текстовое сообщение об ошибке, но и дополнительную информацию.

    public static class ErrorItem {

        private String message;

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

    }

Тогда обработка исключения выглядит следующим образом:

    @ExceptionHandler(NoSuchMusicEndpointException.class)
    public ResponseEntity<ErrorItem> handle(NoSuchMusicEndpointException e) {
        ErrorItem error = new ErrorItem();
        error.setMessage(e.getMessage());

        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

Можно обойтись и без дополнительного класса, а построить полноценный ответ самостоятельно — выбирайте, что вам нравится. Я попробовал несколько способов. И все они вполне неплохо работают.

    @ExceptionHandler(WrongStatusException.class)
    public ResponseEntity<?> handle(WrongStatusException e) {
        String detail = "You can't change the status from " + e.getCurrentStatus() 
                + " to " + e.getNewStatus(); 
        return ResponseEntity
                .status(HttpStatus.METHOD_NOT_ALLOWED)
                .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE)
                .body(Problem.create()
                        .withTitle("Method not allowed")
                        .withDetail(detail));
    }

Пожалуй, на сегодня это всё. Это была очень базовая обработка исключений, но нужно же с чего-то начинать! Да и в моём проекте даже такой код помог значительно упростить другие классы и убрать из них нагромождение блоков try-catch или обозначений throws.

2 Comments

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s