JavaRush /Курси /Модуль 5. Spring /Обробка помилок у Spring MVC

Обробка помилок у Spring MVC

Модуль 5. Spring
Рівень 8 , Лекція 3
Відкрита

Помилки в додатку —, на жаль, річ неминуча. Некоректні вхідні дані від користувача, збої запитів до бази даних, відмова зовнішнього сервісу — усе це стандартні ситуації, які можуть призвести до виключень. Для розробника важливо вміти грамотно обробляти ці помилки, щоб вони не «виливалися» на кінцевого користувача у вигляді лякальних stack-trace'ів.

У Spring MVC для цього є вбудовані механізми:

  • Локальна обробка помилок: за допомогою методів у контролерах.
  • Глобальна обробка помилок: з використанням анотації @ControllerAdvice.

Обробники помилок допоможуть:

  • Сховати внутрішню інформацію (наприклад, stack-trace'и).
  • Показати користувачу зрозуміле й корисне повідомлення про помилку.
  • Сформувати відповідний HTTP-статус код (наприклад, 400 Bad Request або 500 Internal Server Error).
  • Вести централізоване логування помилок.

Локальна обробка помилок

Локальна обробка помилок — це обробка виключень у межах одного контролера. У Spring для цього використовується анотація @ExceptionHandler. Вона дозволяє зв'язати виключення з методом, який має бути викликаний, якщо це виключення виникне.

Приклад локального обробника Розглянемо приклад, у якому ми обробляємо виключення MethodArgumentNotValidException, що виникає під час валідації вхідних даних.


@RestController
@RequestMapping("/api/example")
public class ExampleController {

    @PostMapping("/create")
    public String createExample(@Valid @RequestBody ExampleDto dto) {
        // Твій код обробки запиту
        return "Example created successfully!";
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException exception) {
        // Дістаємо першу помилку валідації, щоб повернути її користувачу
        String errorMessage = exception.getBindingResult()
                                       .getFieldErrors()
                                       .get(0)
                                       .getDefaultMessage();
        return ResponseEntity.badRequest().body("Validation Error: " + errorMessage);
    }
}

У цьому коді:

  • Якщо при валідації об'єкта ExampleDto буде знайдено порушення (наприклад, поле name порожнє), то кине виключення MethodArgumentNotValidException.
  • Обробник @ExceptionHandler перехопить його й поверне користувачу зрозуміле повідомлення про помилку.

Локальна обробка підходить для невеликих проєктів, але стає недостатньою, якщо в системі багато контролерів, і вони мають однаково реагувати на ті самі виключення. У таких випадках зручно використовувати глобальну обробку помилок.


Глобальна обробка помилок

Глобальна обробка помилок за допомогою анотації @ControllerAdvice дозволяє визначити єдиний набір обробників виключень, які застосовуються до всіх контролерів у додатку.

@ControllerAdvice — це спеціальна анотація Spring, яка дозволяє створити централізований обробник виключень. Усі методи, анотовані @ExceptionHandler всередині класу з @ControllerAdvice, будуть викликані для обробки виключень, що виникли в будь-якому контролері додатку.

Приклад глобального обробника Створимо клас для обробки помилок:


@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException exception) {
        // Збираємо повідомлення про всі помилки валідації
        String errorMessage = exception.getBindingResult()
                                       .getFieldErrors()
                                       .stream()
                                       .map(error -> error.getField() + ": " + error.getDefaultMessage())
                                       .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest().body("Validation Errors: " + errorMessage);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception exception) {
        // Логуємо помилку (в реальному проєкті краще використовувати логгер)
        System.err.println("Unexpected error: " + exception.getMessage());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body("An unexpected error occurred. Please try again later.");
    }
}

У цьому коді:

  • Метод handleValidationException обробляє тільки помилки валідації (MethodArgumentNotValidException) і повертає зрозуміле повідомлення, яке агрегує всі помилки.
  • Метод handleGeneralException перехоплює будь-які інші виключення й повертає повідомлення про внутрішню помилку сервера.

Як це працює?

Коли виникає виключення, Spring спочатку перевіряє, чи є локальний обробник у поточному контролері. Якщо його немає, він шукає підходящий метод у класі з анотацією @ControllerAdvice.


Переваги глобальної обробки помилок

Використання @ControllerAdvice має кілька переваг:

  • Централізоване керування обробкою виключень.
  • Повторне використання коду (не треба дублювати логіку обробки помилок у кожному контролері).
  • Можливість використовувати поліморфізм (можна створити ієрархію виключень і обробляти їх по черзі).

Приклад: обробка кастомних виключень

У реальних проєктах часто створюють кастомні виключення для специфічних ситуацій. Наприклад, виключення ResourceNotFoundException для випадків, коли сутність не знайдена.

Створимо клас виключення:


public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Обробка виключення Додамо обробник у GlobalExceptionHandler:


@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException exception) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
}

Використання в контролері

Тепер можна кидати виключення ResourceNotFoundException, якщо ресурс не знайдено:


@GetMapping("/item/{id}")
public Item getItem(@PathVariable Long id) {
    return itemRepository.findById(id)
                         .orElseThrow(() -> new ResourceNotFoundException("Item with ID " + id + " not found"));
}

Якщо елемент із вказаним id відсутній у базі, користувач отримає відповідь з кодом 404 Not Found і повідомлення "Item with ID {id} not found".


Практичні поради

  1. При створенні кастомних виключень уникай "роздмухування" ієрархії класів. Використовуй чітку й лаконічну структуру.
  2. Завжди логуй помилки, але не показуй користувачу внутрішні подробиці (наприклад, SQL-запити або stack-trace'и).
  3. Встановлюй відповідні HTTP-статус коди в відповідях (наприклад, 400 для помилок валідації, 404 для відсутнього ресурсу, 500 для внутрішніх помилок).
  4. Додавай причину помилки в опис відповіді, щоб полегшити діагностику проблем.

Практичне завдання

Завдання: реалізуй глобальний обробник помилок, який:

  1. Обробляє помилки валідації (MethodArgumentNotValidException) і повертає агрегований список помилок.
  2. Обробляє випадки, коли ресурс не знайдено (ResourceNotFoundException).
  3. Ловить усі інші виключення й повертає стандартне повідомлення "Internal Server Error".

Спробуй інтегрувати цей обробник у свій поточний проєкт і протестувати різні сценарії помилок.


На цьому етапі твій код стане більш надійним: ти зможеш показувати користувачу корисну інформацію, а не «гарний» stack-trace з помилками. Тепер будь-яке виключення в системі буде оброблене елегантно й централізовано.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ