Почему не стоит обрабатывать бизнес-исключения через ControllerAdvice для всего контроллера
Что проверяют
Понимаете ли вы, что @ControllerAdvice это транспортный слой, а не место для доменной логики, и отличаете ли техническую ошибку от нормального исхода бизнес-сценария. Вопрос провокационный: глобальный обработчик в Spring полезен, ответ «он плохой» неверен, нужно назвать границы.
Как отвечать
Скажите, что advice без basePackages или assignableTypes действует на все контроллеры приложения. Один класс на весь проект превращается в свалку, где маппинг, добавленный ради одного эндпоинта, молча меняет ответы соседних.
Дальше по сути: бизнес-исключение часто не ошибка, а ожидаемый исход. «Недостаточно средств», «товар закончился», «промокод истёк» это результат сценария, и код ответа под них задаёт контракт конкретного эндпоинта: в одном месте 409, в другом 200 с полем состояния. Глобальный обработчик стирает разницу и отдаёт всем одинаковое.
Аргументы, которые стоит назвать:
- исключение как поток управления прячет исход из сигнатуры: по методу сервиса не видно, что он умеет отказать. Явный результирующий тип честнее;
- создание исключения заполняет стек-трейс, и на горячем пути это дорого. Помогает конструктор с отключённым
writableStackTrace, но сама нужда в нём намекает на неподходящий инструмент; - advice не ловит брошенное вне диспетчера: цепочка фильтров, в том числе Spring Security, проходит мимо, ответ на отказ авторизации собирается отдельно.
Рабочая схема: глобальный advice держит технические и валидационные ошибки и общий формат ответа (ProblemDetail), доменные исходы разбирает контроллер своего модуля.
Где спотыкаются
Заявляют, что глобальная обработка вредит производительности. Диспетчеризация в обработчик стоит ничтожно мало, платите вы за создание самого исключения, и от места обработки это не зависит.
Говорят, что из общего advice непонятно, откуда прилетела ошибка. На это отвечает стек-трейс, потеря в другом: обработчик не знает контекста и не объяснит, какое правило нарушено.
Ловят Exception одним методом ради того, чтобы точно не упало. Такой обработчик перехватывает и то, что Spring разбирает сам, например ошибки валидации тела запроса, и вместо внятного 400 клиент получает 500.