Какой у вас опыт обработки ошибок в Spring MVC @Controller?

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

Ответ

Имею опыт централизованной обработки исключений в Spring MVC с использованием @ControllerAdvice и @ExceptionHandler. Это позволяет отделить бизнес-логику от кода обработки ошибок и возвращать клиенту структурированные, консистентные ответы.

Почему @ControllerAdvice — best practice: Глобальная обработка исключений для всех контроллеров, избегание дублирования кода (try-catch в каждом методе), унификация формата ошибок.

Пример глобального обработчика исключений:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "NOT_FOUND",
            ex.getMessage(),
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.toList());

        ErrorResponse error = new ErrorResponse(
            "VALIDATION_FAILED",
            "Invalid request parameters",
            Instant.now(),
            errors // Детали ошибок валидации
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception ex) {
        // Логируем полный stacktrace для внутреннего анализа
        logger.error("Unexpected error", ex);
        // Клиенту возвращаем общее сообщение без деталей
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_SERVER_ERROR",
            "An internal server error occurred",
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
}

// DTO для ответа с ошибкой
record ErrorResponse(String code, String message, Instant timestamp, List<String> details) {
    public ErrorResponse(String code, String message, Instant timestamp) {
        this(code, message, timestamp, null);
    }
}

Ключевые практики:

  1. Специфичные исключения: Создание собственных иерархий исключений (например, BusinessException, ValidationException).
  2. HTTP-статусы: Корректное сопоставление типа исключения с HTTP-статусом (404 для "Не найдено", 400 для невалидных данных, 409 для конфликтов).
  3. Логирование: Все необработанные исключения (Exception.class) должны логироваться на уровне ERROR для последующего анализа.
  4. Безопасность: В ответах клиенту не должно быть stacktrace или внутренних деталей реализации, которые могут помочь атакующему.