← Назад к вопросам

Как работает global exception handler?

1.8 Middle🔥 231 комментариев
#REST API и микросервисы#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работает global exception handler?

Global Exception Handler в Spring — это централизованный механизм для обработки исключений, выброшенных в приложении. Вместо обработки ошибок в каждом контроллере отдельно, можно определить глобальный обработчик, который перехватит исключения и вернёт унифицированный ответ клиенту.

Архитектура и порядок обработки

Когда в контроллере выбрасывается исключение, Spring DispatcherServlet перехватывает его и пытается найти подходящий обработчик в следующем порядке:

  1. @ExceptionHandler методы в том же контроллере (самый высокий приоритет)
  2. @ControllerAdvice классы с @ExceptionHandler (глобальные обработчики)
  3. Встроенные обработчики Spring (если ничего не найдено)
  4. Стандартная ошибка сервера (500 Internal Server Error)

Основной пример: @ControllerAdvice и @ExceptionHandler

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.time.LocalDateTime;

@ControllerAdvice
public class GlobalExceptionHandler {
    
    // Обработка пользовательского исключения
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "RESOURCE_NOT_FOUND",
            ex.getMessage(),
            HttpStatus.NOT_FOUND.value(),
            LocalDateTime.now()
        );
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(error);
    }
    
    // Обработка ошибок валидации
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(
            IllegalArgumentException ex) {
        ErrorResponse error = new ErrorResponse(
            "INVALID_ARGUMENT",
            ex.getMessage(),
            HttpStatus.BAD_REQUEST.value(),
            LocalDateTime.now()
        );
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(error);
    }
    
    // Обработка всех остальных исключений
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(
            Exception ex) {
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_SERVER_ERROR",
            "Unexpected error occurred",
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            LocalDateTime.now()
        );
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
}

Модель ответа об ошибке

public class ErrorResponse {
    private String code;
    private String message;
    private int status;
    private LocalDateTime timestamp;
    
    public ErrorResponse(String code, String message, int status, LocalDateTime timestamp) {
        this.code = code;
        this.message = message;
        this.status = status;
        this.timestamp = timestamp;
    }
    
    // Getters and setters
    public String getCode() { return code; }
    public String getMessage() { return message; }
    public int getStatus() { return status; }
    public LocalDateTime getTimestamp() { return timestamp; }
}

Пользовательское исключение

public class ResourceNotFoundException extends RuntimeException {
    private final String resourceName;
    private final String fieldName;
    private final Object fieldValue;
    
    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : %s", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }
}

Использование в контроллере

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        UserDTO user = userService.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
        UserDTO user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

Когда вызовется getUser(999) с несуществующим ID, будет выброшено ResourceNotFoundException, перехвачено GlobalExceptionHandler и возвращен JSON:

{
    "code": "RESOURCE_NOT_FOUND",
    "message": "User not found with id : 999",
    "status": 404,
    "timestamp": "2024-03-15T10:30:00"
}

Обработка ошибок валидации (MethodArgumentNotValidException)

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ValidationErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        
        ValidationErrorResponse response = new ValidationErrorResponse(
            "VALIDATION_FAILED",
            errors,
            HttpStatus.BAD_REQUEST.value(),
            LocalDateTime.now()
        );
        
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(response);
    }
}

public class ValidationErrorResponse {
    private String code;
    private Map<String, String> errors;
    private int status;
    private LocalDateTime timestamp;
    
    // Constructor and getters
}

Если отправить невалидный JSON:

POST /api/users
{
    "email": "invalid-email",
    "age": -5
}

Ответ:
{
    "code": "VALIDATION_FAILED",
    "errors": {
        "email": "must be a valid email",
        "age": "must be greater than 0"
    },
    "status": 400,
    "timestamp": "2024-03-15T10:30:00"
}

Обработка исключений на уровне контроллера (локальная)

Можно определить @ExceptionHandler в самом контроллере для более специфичной обработки:

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @GetMapping("/{id}")
    public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
        return ResponseEntity.ok(orderService.findById(id));
    }
    
    // Локальный обработчик имеет приоритет над глобальным
    @ExceptionHandler(OrderNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleOrderNotFound(OrderNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            "ORDER_NOT_FOUND",
            "Order: " + ex.getMessage(),
            HttpStatus.NOT_FOUND.value(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

Обработка исключений с логированием

@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        // Логируем стек вызовов только для неожиданных ошибок
        logger.error("Unexpected exception occurred", ex);
        
        ErrorResponse error = new ErrorResponse(
            "INTERNAL_ERROR",
            "An unexpected error occurred",
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            LocalDateTime.now()
        );
        
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
        // Логируем как warning, т.к. это ожидаемая ошибка
        logger.warn("Resource not found: {}", ex.getMessage());
        
        ErrorResponse error = new ErrorResponse(
            "NOT_FOUND",
            ex.getMessage(),
            HttpStatus.NOT_FOUND.value(),
            LocalDateTime.now()
        );
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
}

Порядок приоритетов обработчиков

Специальность исключения (более специфичное имеет приоритет):

@ControllerAdvice
public class GlobalExceptionHandler {
    
    // Самый специфичный — обрабатывается в первую очередь
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> handleResourceNotFound(ResourceNotFoundException ex) { }
    
    // Менее специфичный
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> handleRuntimeException(RuntimeException ex) { }
    
    // Самый общий — обрабатывается в последнюю очередь
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGenericException(Exception ex) { }
}

Лучшие практики

  1. Определяйте свои исключения — создавайте пользовательские классы для разных типов ошибок
  2. Логируйте достаточно информации — для отладки ошибок в продакшене
  3. Не выдавайте внутренние стеки вызовов клиенту — это уязвимость безопасности
  4. Используйте стандартные HTTP статусы — 400 для Bad Request, 404 для Not Found, 500 для Server Error
  5. Унифицируйте формат ошибок — один стандартный ErrorResponse для всех ошибок
  6. Обрабатывайте исключения проверки аргументов — используйте @Valid для DTO

Таким образом, Global Exception Handler позволяет централизованно и единообразно обрабатывать все ошибки в приложении, повышая качество и читаемость кода.