Как реализовать глобальную обработку исключений в Spring Boot для отправки структурированного ответа клиенту?

«Как реализовать глобальную обработку исключений в Spring Boot для отправки структурированного ответа клиенту?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В Spring Boot глобальная обработка исключений реализуется с помощью аннотации @ControllerAdvice (или @RestControllerAdvice для REST API). Класс, помеченный этой аннотацией, перехватывает исключения, выброшенные в любом контроллере, и позволяет вернуть клиенту структурированный и безопасный ответ.

Базовая реализация:

  1. DTO для ошибки:

    @Data // Lombok аннотация для геттеров/сеттеров
    @AllArgsConstructor
    public class ErrorResponse {
        private String message; // Понятное сообщение для пользователя
        private String details; // Детали (например, ID ошибки, в dev-среде — сообщение исключения)
        private Instant timestamp;
        private String path;
    }
  2. Глобальный обработчик исключений (@RestControllerAdvice):

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(Exception.class) // Перехватывает все неперехваченные исключения
        public ResponseEntity<ErrorResponse> handleAllUncaughtException(
                Exception ex,
                WebRequest request) {
    
            ErrorResponse error = new ErrorResponse(
                "Внутренняя ошибка сервера",
                // В продакшене здесь должен быть ID ошибки, а не сообщение исключения
                ex.getMessage(), 
                Instant.now(),
                request.getDescription(false) // URI запроса
            );
    
            return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(error);
        }
    
        // Специфичная обработка для исключений валидации @Valid
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<ErrorResponse> handleValidationException(
                MethodArgumentNotValidException ex) {
            List<String> details = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.toList());
    
            ErrorResponse error = new ErrorResponse(
                "Ошибка валидации",
                String.join(", ", details),
                Instant.now(),
                ""
            );
            return ResponseEntity.badRequest().body(error);
        }
    
        // Обработка кастомных бизнес-исключений
        @ExceptionHandler(ResourceNotFoundException.class)
        public ResponseEntity<ErrorResponse> handleResourceNotFound(
                ResourceNotFoundException ex) {
            ErrorResponse error = new ErrorResponse(
                "Ресурс не найден",
                ex.getMessage(),
                Instant.now(),
                ""
            );
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
        }
    }

Ключевые моменты:

  • @RestControllerAdvice — это специализация @ControllerAdvice, которая автоматически оборачивает возвращаемое значение в @ResponseBody (для REST).
  • @ExceptionHandler определяет, какой тип исключения будет обработан данным методом.
  • Безопасность: В продакшенной среде никогда не следует возвращать клиенту stack trace или детальные сообщения об ошибках системы. Вместо этого используйте уникальные ID ошибок, которые можно сопоставить с логами на сервере.
  • Порядок обработки: Spring выбирает наиболее специфичный обработчик. Если исключение не соответствует ни одному @ExceptionHandler, оно будет перехвачено обработчиком для Exception.class.