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