Когда в приложении больше одного контроллера, обработка исключений без общего центра превращается в повторяющийся шаблон: один и тот же try/catch в каждом методе. @RestControllerAdvice решает это — один класс перехватывает исключения со всего приложения и превращает их в HTTP-ответы.
Проблема: try/catch в каждом контроллере
Без глобального обработчика контроллер выглядит так:
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable UUID id) {
try {
return ResponseEntity.ok(orderService.findById(id));
} catch (OrderNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
Проблемы очевидны: логика повторяется, формат ответа у каждого разработчика свой, тест на обработку ошибки надо писать отдельно для каждого метода.
Короткая формула: контроллер должен описывать успешный путь; что происходит при ошибке — это сквозная ответственность.
@RestControllerAdvice: один центр
@RestControllerAdvice — это @ControllerAdvice + @ResponseBody. Класс с этой аннотацией применяется ко всем контроллерам в приложении:
@RestControllerAdvice
public class GlobalExceptionHandler {
// методы @ExceptionHandler
}
Spring MVC перехватывает исключение, брошенное из контроллера (или слоя под ним), и передаёт его в подходящий @ExceptionHandler внутри этого класса.
@ExceptionHandler: маппинг исключения на HTTP
Каждый метод @ExceptionHandler отвечает за один или несколько типов исключений:
@ExceptionHandler(OrderNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ProblemDetail handleNotFound(OrderNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ProblemDetail handleBadRequest(IllegalArgumentException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
}
ProblemDetail — стандартный формат ответа об ошибке (RFC 9457), встроенный в Spring Boot 3. Подробнее о структуре ответа — в статье REST-ошибки и Problem Details.
Маппинг доменных исключений на HTTP-коды
Типовая схема: доменный слой бросает семантичное исключение, обработчик переводит его в HTTP-код. Сам контроллер ничего не знает о статусах:
| Доменное исключение | HTTP-статус |
|---|---|
NotFoundException | 404 Not Found |
ConflictException | 409 Conflict |
AccessDeniedException | 403 Forbidden |
ValidationException | 400 Bad Request |
Базовый иерархический подход — одна иерархия исключений в модуле домена, один метод @ExceptionHandler на базовый тип:
@ExceptionHandler(DomainException.class)
public ResponseEntity<ProblemDetail> handleDomain(DomainException ex) {
HttpStatus status = ex.getHttpStatus(); // метод на базовом классе
ProblemDetail body = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
return ResponseEntity.status(status).body(body);
}
Что логировать в обработчике
Правило: логировать там, где исключение обрабатывается, а не там, где оно бросается. Уровень зависит от серьёзности:
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ProblemDetail handleUnexpected(Exception ex, HttpServletRequest request) {
log.error("Unexpected error: {} {}", request.getMethod(), request.getRequestURI(), ex);
return ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(OrderNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ProblemDetail handleNotFound(OrderNotFoundException ex) {
log.debug("Order not found: {}", ex.getMessage());
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
ERROR/WARN— неожиданные исключения, сбои инфраструктуры.DEBUG— ожидаемые доменные ошибки (не найдено, конфликт): их можно включить при отладке, но не засорять ими production-логи.- Стектрейс — только на уровне
ERROR; вINFO/DEBUGон не нужен.
Коротко
@RestControllerAdvice— единая точка, где исключения превращаются в HTTP-ответы; контроллеры остаются чистыми.@ExceptionHandlerвнутри него связывает тип исключения с HTTP-статусом и телом ответа.- Доменный слой бросает семантичные исключения;
GlobalExceptionHandlerпереводит их в HTTP-коды — слои не смешиваются. ProblemDetail(Spring Boot 3, RFC 9457) — стандартный формат тела ошибки.- Логируйте в обработчике:
ERRORдля неожиданного,DEBUGдля ожидаемого; стектрейс только приERROR. - Один обработчик на всё приложение проще тестировать:
@WebMvcTest+MockMvcпокрывает сценарии ошибок централизованно.
Что почитать дальше
- Модель ошибок и Problem Details — как выбрать структуру тела ошибки и когда нужны расширения.
- Типичные ошибки при обработке исключений — антипаттерны, которые прячут проблемы вместо их решения.
- REST-ошибки и Problem Details — HTTP-коды, заголовки и формат ответа для REST API.