Помилки — невід’ємна частина будь-якої системи, а в програмуванні тим більше. Замість того, щоб дозволяти виняткам «виплескуватися» на користувача у вигляді незрозумілих повідомлень про помилку або, що ще гірше, стек-трейсів, ми можемо перехоплювати їх і акуратно обробляти. Ви вже робили так у чистій Java. А у Spring для цього є інструмент під назвою @ExceptionHandler.
@ExceptionHandler дозволяє обробляти винятки на рівні конкретного контролера. Це значно спрощує керування помилками і допомагає забезпечити напрацьовану поведінку додатка коли виникають виключення. Наприклад, замість того, щоб показувати користувачу лякаюче повідомлення NullPointerException, ми можемо повернути акуратну JSON-відповідь з описом проблеми.
Основи роботи з @ExceptionHandler
Анотація @ExceptionHandler застосовується до методу в контролері і вказує, які виключення цей метод повинен обробляти. Ось як це виглядає:
@RestController
@RequestMapping("/api")
public class ExampleController {
@GetMapping("/example")
public String exampleEndpoint() {
// Кинемо виняток для демонстрації
throw new IllegalArgumentException("Невірний параметр!");
}
// Обробка виключення IllegalArgumentException
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Помилка: " + ex.getMessage());
}
}
Тут ми визначили метод, помічений @ExceptionHandler, який перехоплює виключення IllegalArgumentException і повертає користувачу читабельне повідомлення про помилку з HTTP-статусом 400 (BAD_REQUEST).
Ключові моменти:
- Метод з
@ExceptionHandlerможе приймати як аргумент об'єкт виключення, щоб отримати додаткову інформацію. - Ви можете повернути будь-який підходящий тип даних (
ResponseEntity,String,Map<String, Object>тощо). - Методу нічого не заважає обробляти кілька типів виключень одразу (наприклад, через суперкласи або масив класів).
Приклад використання @ExceptionHandler
А тепер розберемо реальний приклад. Уявімо, що у нас є додаток для керування книгами. Воно має повертати інформацію про книгу за її ID.
Контролер:
@RestController
@RequestMapping("/books")
public class BookController {
private final Map<Long, String> books = Map.of(
1L, "Java для початківців",
2L, "Просунута Java",
3L, "Spring в дії");
@GetMapping("/{id}")
public String getBook(@PathVariable Long id) {
if (!books.containsKey(id)) {
throw new BookNotFoundException("Книга з ID " + id + " не знайдена.");
}
return books.get(id);
}
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<String> handleBookNotFoundException(BookNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
Користувацький клас виключення:
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(String message) {
super(message);
}
}
Тепер, якщо користувач відправить GET-запит до /books/10, він отримає акуратну відповідь:
HTTP Status: 404 Not Found
Body: "Книга з ID 10 не знайдена."
Переваги використання @ExceptionHandler
- Локальна обробка: метод обробляє виключення лише для цього контролера. Це зручно, якщо різні контролери мають свої специфічні сценарії обробки.
- Чистота коду: винесення обробки виключень в окремі методи допомагає тримати основний код контролера компактним.
- Розширюваність: легко додавати обробку нових виключень у міру потреби.
Обробка кількох виключень з використанням @ExceptionHandler
Іноді один метод може обробляти кілька типів виключень одночасно. Це можливо, бо анотація @ExceptionHandler приймає масив класів виключень.
Приклад:
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
public ResponseEntity<String> handleMultipleExceptions(Exception ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Сталася помилка: " + ex.getMessage());
}
Тут один метод перехоплює і IllegalArgumentException, і NullPointerException, повертаючи однакову відповідь для обох ситуацій.
Типові помилки при використанні @ExceptionHandler
- Неперехоплені виключення: якщо виключення не оброблене в поточному контролері, воно "спливе" вгору, де може бути перехоплене глобальним обробником або взагалі лишитися необробленим.
- Перекривання стандартної логіки Spring: деякі виключення, такі як
MethodArgumentNotValidException, вже обробляються Spring за замовчуванням. Якщо ви напишете власний обробник для таких виключень, переконайтеся, що він покриває всі можливі випадки. - Невірний HTTP статус: переконайтеся, що повернутий статус відповідає логіці REST API. Наприклад,
NOT_FOUNDдля відсутніх ресурсів іBAD_REQUESTдля помилок валідації.
Практичне завдання
- Створіть контролер для керування користувачами, де буде метод, що повертає інформацію про користувача за його ID.
- Реалізуйте користувацьке виключення
UserNotFoundException. - Додайте обробник
@ExceptionHandlerдля цього виключення, який повертає HTTP-статус 404 і повідомлення про помилку. - Переконайтеся, що обробник коректно працює, відправивши запит з неіснуючим ID користувача.
Ось який прикладний результат повинен вийти:
Запит:
GET /users/99
Відповідь:
HTTP Status: 404 Not Found
Body: "Користувач з ID 99 не знайдений."
Підсумкові зауваження
@ExceptionHandler — потужний інструмент для обробки виключень на рівні контролерів. Він дозволяє писати чистий і лаконічний код, полегшуючи діагностику і покращуючи UX. Використовуйте його з розумом, щоб ваші користувачі ніколи не бачили пресловуте "Internal Server Error" і не побігли до конкурентів! ;)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ