← Назад к вопросам
Какие знаешь инструменты для обработки ошибок в @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
- Иерархия исключений — создавай кастомные исключения для бизнес-логики
- Логирование — всегда логируй исключения с полным стеком
- HTTP статусы — используй правильные HTTP коды ответов
- Безопасность — не раскрывай детали внутренней реализации в ошибках
- Трассировка — добавляй трассировки (trace ID) для отладки
- Структурированные ответы — используй одинаковый формат для всех ошибок
- Валидация — используй @Valid для входных данных
Сравнение подходов
| Инструмент | Область применения | Преимущества |
|---|---|---|
| @ExceptionHandler | Одиночный контроллер | Простота, контроль |
| @ControllerAdvice | Глобальная обработка | Централизация, переиспользование |
| @ResponseStatus | Простые случаи | Минимум кода |
| ResponseStatusException | Встроенные исключения | Встроено в Spring |
| Problem Details | RFC стандарт | Стандартизация, клиентская совместимость |
В production я использую комбинацию @RestControllerAdvice для централизованной обработки с кастомными исключениями для специфичной бизнес-логики.