← назад к разделу

Когда в приложении больше одного контроллера, обработка исключений без общего центра превращается в повторяющийся шаблон: один и тот же 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-статус
NotFoundException404 Not Found
ConflictException409 Conflict
AccessDeniedException403 Forbidden
ValidationException400 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.