Сегодня пришла очередь рассмотреть обработку исключений в 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