Глобальная обработка ошибок: @RestControllerAdvice и ProblemDetail
Вместо try-catch в каждом методе ошибки обрабатываются в одном месте — @RestControllerAdvice. Современный формат ответа об ошибке — ProblemDetail по RFC 7807.
Суть: @RestControllerAdvice ловит исключения со всех контроллеров и превращает их в аккуратные HTTP-ответы. ProblemDetail задаёт стандартную структуру тела ошибки.
Без централизованной обработки код контроллеров обрастает блоками try-catch, а клиенты получают разношёрстные ошибки: где-то строка, где-то стектрейс, где-то пусто. Spring предлагает вынести всю обработку в один класс-советник.
Глобальный обработчик
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ProblemDetail handleNotFound(UserNotFoundException ex) {
ProblemDetail pd = ProblemDetail
.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Пользователь не найден");
return pd;
}
@ExceptionHandler(IllegalArgumentException.class)
public ProblemDetail handleBadRequest(IllegalArgumentException ex) {
return ProblemDetail
.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
}
}
@RestControllerAdvice — это «зонтик» над всеми контроллерами. Метод с @ExceptionHandler ловит указанный тип исключения, откуда бы оно ни прилетело, и формирует ответ.
ProblemDetail — стандарт ошибок
ProblemDetail появился в Spring 6 и реализует RFC 7807. Он задаёт единый формат JSON для ошибок:
// Ответ ProblemDetail сериализуется в такой JSON:
// {
// "type": "about:blank",
// "title": "Пользователь не найден",
// "status": 404,
// "detail": "Нет пользователя с id 7"
// }
Клиент всегда знает: будет поле status, title и detail. Это убирает разнобой в ошибках по всему API.
Как работает под капотом
Когда из метода контроллера вылетает исключение, DispatcherServlet ищет подходящий @ExceptionHandler — сперва в самом контроллере, затем в глобальных советниках. Найдя обработчик по типу исключения, он вызывает его и использует результат как ответ.
Controller бросает UserNotFoundException
|
v
DispatcherServlet ловит исключение
|
v
ищет @ExceptionHandler по типу
локальный? -> глобальный @RestControllerAdvice
|
v
handleNotFound() -> ProblemDetail (404)
|
v
стандартный JSON клиенту
Смоделируем диспетчер обработчиков ошибок:
# Сопоставление типа ошибки с обработчиком, как в @RestControllerAdvice
class UserNotFound(Exception): pass
class BadInput(Exception): pass
def problem_detail(status, title, detail):
return {"status": status, "title": title, "detail": detail}
handlers = {
UserNotFound: lambda e: problem_detail(404, "Не найдено", str(e)),
BadInput: lambda e: problem_detail(400, "Неверный ввод", str(e)),
}
def dispatch(exc):
for exc_type, handler in handlers.items():
if isinstance(exc, exc_type):
return handler(exc)
return problem_detail(500, "Внутренняя ошибка", "Неизвестно")
print(dispatch(UserNotFound("Нет пользователя с id 7")))
print(dispatch(BadInput("Возраст отрицательный")))
print(dispatch(RuntimeError("сбой")))
Нажмите «Попробуй сам ▶» — каждая ошибка маршрутизируется к своему обработчику и превращается в структурированный ответ.
Частые ошибки
- Глотать исключения. Пустой catch без логирования и ответа прячет проблему от клиента и разработчика.
- Отдавать стектрейс клиенту. Внутренние детали — в логи, клиенту — аккуратный ProblemDetail.
- Один обработчик на Exception.class. Слишком общий перехват маскирует разные ошибки одинаковым статусом.
Best practices
- Заведите свои исключения предметной области (
UserNotFoundException) и маппьте их на статусы. - Используйте
ProblemDetailкак единый формат ошибок (RFC 7807). - Логируйте 5xx с деталями, клиенту отдавайте безопасное сообщение.
Итог: @RestControllerAdvice централизует обработку ошибок, а ProblemDetail делает их единообразными. Контроллеры остаются чистыми, клиент получает предсказуемые ответы.
Закрепим главное
Централизованная обработка ошибок — это про чистоту и единообразие одновременно. Контроллеры остаются тонкими, потому что не обрастают блоками try-catch, а клиент получает ошибки в одном предсказуемом формате независимо от того, в каком уголке приложения они возникли. Заведите небольшой набор собственных исключений предметной области и сопоставьте каждое со своим статусом — это сделает поведение API очевидным.
Отдельно держите в голове границу между тем, что видит клиент, и тем, что попадает в логи. Стектрейсы, имена классов и SQL — это внутренняя кухня, её место в логах сервера. Клиенту достаётся аккуратный ProblemDetail с понятным сообщением и кодом. Такое разделение одновременно повышает безопасность (вы не раскрываете внутреннее устройство) и удобство (клиент видит человеко-читаемую причину). Для ошибок 5xx обязательно логируйте детали, иначе диагностировать сбой в проде будет нечем.