Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Кастомные исключения в Java: зачем они нужны
Кастомные исключения — это один из мощнейших инструментов для написания чистого, понятного и maintainable кода. Это не просто «красивость», а архитектурное решение, которое серьёзно влияет на качество приложения.
Семантика и выразительность
Основная причина: кастомные исключения позволяют выразить конкретную ошибку вашего домена. Вместо того, чтобы писать:
// Плохо: непонятно, что пошло не так
try {
user = userRepository.findById(id);
} catch (Exception e) {
// Что случилось? NoSQL исключение? Timeout? Parsing ошибка?
}
Можно написать:
// Хорошо: ясная семантика
public class UserNotFoundException extends RuntimeException {
private final UUID userId;
public UserNotFoundException(UUID userId) {
super("User not found with id: " + userId);
this.userId = userId;
}
public UUID getUserId() {
return userId;
}
}
// Использование
try {
user = userRepository.findById(id);
} catch (UserNotFoundException e) {
// Точно знаю, что это конкретный случай
log.warn("Attempted access to non-existent user: {}", e.getUserId());
return ResponseEntity.notFound().build();
}
Код сразу становится понятнее, потому что ошибка имеет доменное название.
Обработка разных типов ошибок
Это критично в сложных системах. Разные ошибки требуют разных действий:
public class PaymentService {
public void processPayment(Order order) {
try {
validateOrder(order); // может выбросить ValidationException
charge(order); // может выбросить PaymentGatewayException
updateInventory(order); // может выбросить InventoryException
} catch (ValidationException e) {
// Это ошибка клиента — вернуть 400
throw new BadRequestException(e.getMessage(), e);
} catch (PaymentGatewayException e) {
// Это ошибка системы — логировать, отправить алерт
log.error("Payment gateway failed", e);
notifyOps("Payment processing failed: " + e.getMessage());
throw new PaymentProcessingException("Payment failed, please try again", e);
} catch (InventoryException e) {
// Откатить платёж, вернуть средства
chargeService.refund(order);
throw new InventoryException("Out of stock, refund processed", e);
}
}
}
Высокоуровневый код может красиво обрабатывать разные сценарии, потому что исключения кодируют информацию об ошибке.
REST контроллер пример
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@PostMapping
public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
try {
Order order = orderService.createOrder(request);
return ResponseEntity.ok(new OrderResponse(order));
} catch (InvalidOrderException e) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("Invalid order: " + e.getMessage()));
} catch (UserNotFoundException e) {
return ResponseEntity.notFound().build();
} catch (OutOfStockException e) {
return ResponseEntity.status(409)
.body(new ErrorResponse("Out of stock: " + e.getProductId()));
} catch (PaymentProcessingException e) {
return ResponseEntity.status(500)
.body(new ErrorResponse("Payment failed, please try again"));
}
}
}
Каждое исключение соответствует HTTP статус коду и определённому ответу.
Иерархия исключений как архитектура
Кастомные исключения должны иметь иерархию:
// Базовое доменное исключение
public abstract class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
public DomainException(String message, Throwable cause) {
super(message, cause);
}
}
// Ошибки валидации
public class ValidationException extends DomainException {
public ValidationException(String message) {
super(message);
}
}
public class InvalidOrderException extends ValidationException {
private final List<String> errors;
public InvalidOrderException(List<String> errors) {
super("Order validation failed: " + String.join(", ", errors));
this.errors = errors;
}
public List<String> getErrors() {
return errors;
}
}
// Ошибки ресурсов
public class ResourceNotFoundException extends DomainException {
public ResourceNotFoundException(String resourceType, String id) {
super(resourceType + " not found: " + id);
}
}
// Ошибки бизнес-логики
public class BusinessLogicException extends DomainException {
public BusinessLogicException(String message) {
super(message);
}
}
public class InsufficientFundsException extends BusinessLogicException {
private final BigDecimal available;
private final BigDecimal required;
public InsufficientFundsException(BigDecimal available, BigDecimal required) {
super("Insufficient funds: available " + available + ", required " + required);
this.available = available;
this.required = required;
}
}
Теперь выше по стеку можно ловить по иерархии:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<?> handleValidation(ValidationException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleNotFound(ResourceNotFoundException e) {
return ResponseEntity.notFound().build();
}
@ExceptionHandler(BusinessLogicException.class)
public ResponseEntity<?> handleBusinessLogic(BusinessLogicException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse(e.getMessage()));
}
}
Отладка и логирование
Кастомные исключения облегчают отладку:
public class PaymentProcessingException extends DomainException {
private final String transactionId;
private final String paymentGateway;
private final long timestamp;
public PaymentProcessingException(
String message,
String transactionId,
String paymentGateway,
Throwable cause) {
super(message, cause);
this.transactionId = transactionId;
this.paymentGateway = paymentGateway;
this.timestamp = System.currentTimeMillis();
}
public String toLogString() {
return String.format(
"Payment failed [txId=%s, gateway=%s, timestamp=%d]: %s",
transactionId, paymentGateway, timestamp, getMessage()
);
}
}
// Логирование
log.error(exception.toLogString(), exception);
Выводы
Кастомные исключения:
- Улучшают читаемость — код ясно выражает, что может пойти не так
- Упрощают обработку — разные ошибки обрабатываются по-разному
- Документируют API — вызывающий код знает, какие исключения ожидать
- Облегчают отладку — содержат релевантный контекст ошибки
- Архитектурны — правильная иерархия отражает структуру системы
Не используй checked исключения для доменных ошибок (наследуй от RuntimeException), чтобы не усложнять API.