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

Ответ

Имею опыт централизованной обработки исключений в 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 или внутренних деталей реализации, которые могут помочь атакующему.

Ответ 18+ 🔞

А, слушай, вот это тема, про которую я могу рассказывать часами, пока ты не захочешь меня самого в озеро с мешком выкинуть, как ту Муму! Централизованная обработка исключений в Spring — это просто пиздец как удобно, если, конечно, не накосячить.

Представь себе: у тебя двадцать контроллеров, и в каждом ты пишешь try-catch, чтобы отловить, что какой-то Resource не нашелся. Это ж какая ж блядь заложила такую архитектуру? Это же рутина, от которой мозг вскипает, как чайник у бабки! А потом ещё и формат ошибок в каждом методе разный — один возвращает просто строку, другой — JSON-объект, третий вообще молча 500-ку шлёт. Пиздец, бардак, одним словом.

И тут выходит на сцену, блядь, @ControllerAdvice! Это как Герасим, только не немой, а наоборот, орет на все ваши исключения одним махом. Вместо того чтобы в каждом методе контроллера писать одно и то же, ты создаёшь один класс-обработчик, который ловит всё, что летит из контроллеров, и приводит к одному знаменателю. Красота, ёпта!

Вот смотри, как это выглядит, если без соплей:

@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); // Чётко 404, без вариантов
    }

    // А это когда клиент прислал какую-то хуйню вместо валидных данных
    @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); // 400 — сам дурак
    }

    // А это наш "ковёр-самолёт", который ловит всё остальное, что не поймали выше
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception ex) {
        // Вот тут ОБЯЗАТЕЛЬНО логируем всё в хвост и в гриву, иначе потом не найдешь, откуда ноги растут
        logger.error("Unexpected error", ex);
        // А клиенту показываем только общую отмазку, без stacktrace, чтобы не светить внутренности
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_SERVER_ERROR",
            "An internal server error occurred",
            Instant.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); // 500 — наши проблемы
    }
}

// А это просто DTO, чтобы красиво упаковать ответ. Record — это просто песня, блядь!
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. Свои исключения — наше всё. Не надо кидать голый RuntimeException. Создай BusinessException, ValidationException, ResourceNotFoundException. Так и код чище, и в обработчике сразу понятно, что делать. Это как не говорить "Муму" на всё подряд, а иметь отдельные слова для разных ситуаций.
  2. HTTP-статусы — это святое. Если ресурс не найден — это 404, а не 200 с сообщением "ой, прости". Если данные кривые — 400. Если два клиента пытаются одновременно обновить одно и то же и возникает конфликт — 409. Spring сам много чего мапит, но за своим зоопарком исключений нужно следить.
  3. Логируй, блядь, всё неожиданное. Обработчик для Exception.class — это твоя последняя линия обороны. Всё, что туда попало, — это ЧП. Логируй на уровне ERROR с полным стектрейсом, чтобы потом не гадать, что за хуйня сломалась в проде.
  4. Безопасность, ёпта! Никогда, блядь, НИКОГДА не пробрасывай stacktrace, имена классов или детали БД в ответе клиенту. Это манна небесная для того, кто захочет тебя взломать. Клиенту — вежливая, общая отмазка. А себе в логи — все кровавые подробности.

Вот когда это всё настроено, жить становится проще. Контроллеры чистые, ответы одинаковые, а в логах — порядок. Просто ёперный театр, а не архитектура!