Как работает global exception handler?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает global exception handler?
Global Exception Handler в Spring — это централизованный механизм для обработки исключений, выброшенных в приложении. Вместо обработки ошибок в каждом контроллере отдельно, можно определить глобальный обработчик, который перехватит исключения и вернёт унифицированный ответ клиенту.
Архитектура и порядок обработки
Когда в контроллере выбрасывается исключение, Spring DispatcherServlet перехватывает его и пытается найти подходящий обработчик в следующем порядке:
- @ExceptionHandler методы в том же контроллере (самый высокий приоритет)
- @ControllerAdvice классы с @ExceptionHandler (глобальные обработчики)
- Встроенные обработчики Spring (если ничего не найдено)
- Стандартная ошибка сервера (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) { }
}
Лучшие практики
- Определяйте свои исключения — создавайте пользовательские классы для разных типов ошибок
- Логируйте достаточно информации — для отладки ошибок в продакшене
- Не выдавайте внутренние стеки вызовов клиенту — это уязвимость безопасности
- Используйте стандартные HTTP статусы — 400 для Bad Request, 404 для Not Found, 500 для Server Error
- Унифицируйте формат ошибок — один стандартный ErrorResponse для всех ошибок
- Обрабатывайте исключения проверки аргументов — используйте @Valid для DTO
Таким образом, Global Exception Handler позволяет централизованно и единообразно обрабатывать все ошибки в приложении, повышая качество и читаемость кода.