JavaRush /Курсы /Модуль 5. Spring /Обработка ошибок в Spring MVC

Обработка ошибок в Spring MVC

Модуль 5. Spring
9 уровень , 3 лекция
Открыта

Ошибки в приложении — к сожалению, штука неизбежная. Некорректные пользовательские входные данные, сбои запроса в базе данных, отказ внешнего сервиса — всё это стандартные ситуации, которые могут привести к исключениям. Для разработчика важно уметь грамотно обрабатывать эти ошибки, чтобы они не "падали" на конечного пользователя в виде пугающих стек-трейсов.

У Spring MVC для этих целей есть встроенные механизмы:

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

Обработчики ошибок помогут:

  • Скрыть внутреннюю информацию (например, стек-трейсы).
  • Показать пользователю понятное и полезное сообщение об ошибке.
  • Сформировать соответствующий 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-запросы или стек-трейсы).
  3. Устанавливайте подходящие HTTP-статус коды в ответах (например, 400 для ошибок валидации, 404 для отсутствия ресурса, 500 для внутренних ошибок).
  4. Добавляйте причину ошибки в описание ответа, чтобы облегчить диагностику проблем.

Практическая задача

Задача: реализуйте глобальный обработчик ошибок, который:

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

Попробуйте интегрировать этот обработчик в свой текущий проект и протестировать различные сценарии ошибок.


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

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ