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

Какие знаешь инструменты для обработки ошибок в @Controller?

2.0 Middle🔥 201 комментариев
#Spring Framework

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

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

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

Инструменты обработки ошибок в Spring @Controller

Обработка исключений в контроллерах — критичная часть разработки REST API. Spring предоставляет несколько мощных инструментов для элегантной и централизованной обработки ошибок.

1. @ExceptionHandler — Локальная обработка исключений

Обрабатывает исключения на уровне конкретного контроллера:

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
  
  private final ProductService productService;
  
  @GetMapping("/{id}")
  public Product getProduct(@PathVariable Long id) {
    return productService.findById(id)
      .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id));
  }
  
  // Обработчик исключения локально в контроллере
  @ExceptionHandler(ProductNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  public ErrorResponse handleProductNotFound(ProductNotFoundException ex) {
    return new ErrorResponse(
      HttpStatus.NOT_FOUND.value(),
      ex.getMessage(),
      System.currentTimeMillis()
    );
  }
  
  // Обработка множественных исключений
  @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ErrorResponse handleValidationErrors(Exception ex) {
    return new ErrorResponse(
      HttpStatus.BAD_REQUEST.value(),
      "Validation failed: " + ex.getMessage(),
      System.currentTimeMillis()
    );
  }
}

public class ProductNotFoundException extends RuntimeException {
  public ProductNotFoundException(String message) {
    super(message);
  }
}

public class ErrorResponse {
  private int status;
  private String message;
  private long timestamp;
  
  public ErrorResponse(int status, String message, long timestamp) {
    this.status = status;
    this.message = message;
    this.timestamp = timestamp;
  }
  
  // Getters
}

2. @ControllerAdvice — Глобальная обработка исключений

Централизованная обработка для всех контроллеров:

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
  
  // Обработка исключений валидации
  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  public ErrorResponse handleValidationExceptions(
      MethodArgumentNotValidException ex) {
    
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error ->
      errors.put(error.getField(), error.getDefaultMessage())
    );
    
    return new ErrorResponse(
      HttpStatus.BAD_REQUEST.value(),
      "Validation failed",
      System.currentTimeMillis(),
      errors
    );
  }
  
  // Обработка EntityNotFoundException
  @ExceptionHandler(EntityNotFoundException.class)
  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ResponseBody
  public ErrorResponse handleEntityNotFound(EntityNotFoundException ex) {
    log.warn("Entity not found: {}", ex.getMessage());
    return new ErrorResponse(
      HttpStatus.NOT_FOUND.value(),
      ex.getMessage(),
      System.currentTimeMillis()
    );
  }
  
  // Обработка общих исключений
  @ExceptionHandler(Exception.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ResponseBody
  public ErrorResponse handleGlobalException(Exception ex) {
    log.error("Unexpected error occurred", ex);
    return new ErrorResponse(
      HttpStatus.INTERNAL_SERVER_ERROR.value(),
      "An unexpected error occurred",
      System.currentTimeMillis()
    );
  }
  
  // Обработка DataIntegrityViolationException (нарушение constraints)
  @ExceptionHandler(DataIntegrityViolationException.class)
  @ResponseStatus(HttpStatus.CONFLICT)
  @ResponseBody
  public ErrorResponse handleDataIntegrityViolation(
      DataIntegrityViolationException ex) {
    log.error("Data integrity violation", ex);
    return new ErrorResponse(
      HttpStatus.CONFLICT.value(),
      "Data integrity violation. Duplicate or invalid data.",
      System.currentTimeMillis()
    );
  }
}

3. RestControllerAdvice — Для REST API

Удобный вариант @ControllerAdvice специально для REST контроллеров:

@RestControllerAdvice
public class ApiExceptionHandler {
  
  @ExceptionHandler(ProductNotFoundException.class)
  public ResponseEntity<ErrorResponse> handleProductNotFound(
      ProductNotFoundException ex,
      HttpServletRequest request) {
    
    ErrorResponse response = ErrorResponse.builder()
      .status(HttpStatus.NOT_FOUND.value())
      .message(ex.getMessage())
      .path(request.getRequestURI())
      .timestamp(LocalDateTime.now(UTC))
      .build();
    
    return ResponseEntity
      .status(HttpStatus.NOT_FOUND)
      .body(response);
  }
}

4. Кастомные исключения с аннотациями

Использование аннотаций для описания исключений:

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
  public ResourceNotFoundException(String message) {
    super(message);
  }
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidRequestException extends RuntimeException {
  public InvalidRequestException(String message) {
    super(message);
  }
}

// Использование
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
  return ResponseEntity.ok(
    productService.findById(id)
      .orElseThrow(() -> new ResourceNotFoundException(
        "Product with id " + id + " not found"
      ))
  );
}

5. Problem Details (RFC 7807)

Стандартный формат для обработки ошибок:

@RestControllerAdvice
public class ProblemDetailsExceptionHandler {
  
  @ExceptionHandler(ProductNotFoundException.class)
  public ProblemDetail handleProductNotFound(
      ProductNotFoundException ex,
      HttpServletRequest request) {
    
    ProblemDetail detail = ProblemDetail.forStatusAndDetail(
      HttpStatus.NOT_FOUND,
      ex.getMessage()
    );
    detail.setTitle("Product Not Found");
    detail.setProperty("path", request.getRequestURI());
    detail.setProperty("timestamp", LocalDateTime.now(UTC));
    
    return detail;
  }
}

// Также можно создать кастомный ProblemDetail
public class CustomProblemDetail extends ProblemDetail {
  private String traceId;
  private List<FieldError> fieldErrors;
  
  public CustomProblemDetail(HttpStatus status, String detail) {
    super(status.value());
    setDetail(detail);
  }
}

6. ResponseStatusException — Встроенные исключения

Для простых случаев без создания кастомных исключений:

@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
  return productService.findById(id)
    .orElseThrow(() -> new ResponseStatusException(
      HttpStatus.NOT_FOUND,
      "Product not found with id: " + id
    ));
}

@PostMapping
public ResponseEntity<Product> createProduct(
    @Valid @RequestBody CreateProductRequest request) {
  
  if (!isValidPrice(request.getPrice())) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      "Price must be positive"
    );
  }
  
  Product product = productService.create(request);
  return ResponseEntity.status(HttpStatus.CREATED).body(product);
}

7. Comprehensive Error Handling Example

Полный пример обработки ошибок:

// Custom DTO для ошибки
public class ApiError {
  private int status;
  private String message;
  private String path;
  private LocalDateTime timestamp;
  private Map<String, List<String>> fieldErrors;
  private String traceId;
  
  // Конструкторы и getters
}

// Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalApiExceptionHandler {
  
  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ApiError handleMethodArgumentNotValid(
      MethodArgumentNotValidException ex,
      HttpServletRequest request) {
    
    Map<String, List<String>> fieldErrors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error -> {
      String fieldName = error.getField();
      String errorMessage = error.getDefaultMessage();
      fieldErrors.computeIfAbsent(fieldName, k -> new ArrayList<>())
        .add(errorMessage);
    });
    
    return ApiError.builder()
      .status(HttpStatus.BAD_REQUEST.value())
      .message("Validation failed")
      .path(request.getRequestURI())
      .timestamp(LocalDateTime.now(UTC))
      .fieldErrors(fieldErrors)
      .traceId(UUID.randomUUID().toString())
      .build();
  }
  
  @ExceptionHandler(HttpMessageNotReadableException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ApiError handleHttpMessageNotReadable(
      HttpMessageNotReadableException ex,
      HttpServletRequest request) {
    
    log.error("Invalid request body format", ex);
    
    return ApiError.builder()
      .status(HttpStatus.BAD_REQUEST.value())
      .message("Invalid request body format: " + ex.getMessage())
      .path(request.getRequestURI())
      .timestamp(LocalDateTime.now(UTC))
      .traceId(UUID.randomUUID().toString())
      .build();
  }
  
  @ExceptionHandler(AccessDeniedException.class)
  @ResponseStatus(HttpStatus.FORBIDDEN)
  public ApiError handleAccessDenied(
      AccessDeniedException ex,
      HttpServletRequest request) {
    
    log.warn("Access denied for user: {}", request.getUserPrincipal());
    
    return ApiError.builder()
      .status(HttpStatus.FORBIDDEN.value())
      .message("Access denied")
      .path(request.getRequestURI())
      .timestamp(LocalDateTime.now(UTC))
      .build();
  }
  
  @ExceptionHandler(Exception.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public ApiError handleGlobalException(
      Exception ex,
      HttpServletRequest request) {
    
    String traceId = UUID.randomUUID().toString();
    log.error("Unexpected error [traceId: {}]", traceId, ex);
    
    return ApiError.builder()
      .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
      .message("Internal server error. Please contact support.")
      .path(request.getRequestURI())
      .timestamp(LocalDateTime.now(UTC))
      .traceId(traceId)
      .build();
  }
}

// Контроллер
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
  
  @PostMapping
  public ResponseEntity<ProductDTO> createProduct(
      @Valid @RequestBody CreateProductRequest request) {
    Product product = productService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED)
      .body(new ProductDTO(product));
  }
}

Best Practices

  1. Иерархия исключений — создавай кастомные исключения для бизнес-логики
  2. Логирование — всегда логируй исключения с полным стеком
  3. HTTP статусы — используй правильные HTTP коды ответов
  4. Безопасность — не раскрывай детали внутренней реализации в ошибках
  5. Трассировка — добавляй трассировки (trace ID) для отладки
  6. Структурированные ответы — используй одинаковый формат для всех ошибок
  7. Валидация — используй @Valid для входных данных

Сравнение подходов

ИнструментОбласть примененияПреимущества
@ExceptionHandlerОдиночный контроллерПростота, контроль
@ControllerAdviceГлобальная обработкаЦентрализация, переиспользование
@ResponseStatusПростые случаиМинимум кода
ResponseStatusExceptionВстроенные исключенияВстроено в Spring
Problem DetailsRFC стандартСтандартизация, клиентская совместимость

В production я использую комбинацию @RestControllerAdvice для централизованной обработки с кастомными исключениями для специфичной бизнес-логики.

Какие знаешь инструменты для обработки ошибок в @Controller? | PrepBro