← Назад к вопросам
Как была устроена обработка исключений на прошлой работе
1.0 Junior🔥 111 комментариев
#Soft Skills и карьера
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как была устроена обработка исключений на прошлой работе
Контекст
На прошлой работе я разрабатывал микросервис обработки платежей (платформа e-commerce). Я помогу привести реалистичный пример, как мы структурировали обработку исключений.
Слои обработки исключений
1. Домен (Domain Layer)
Мы определили свои исключения для бизнес-логики:
// Базовое исключение для доменного слоя
public abstract class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
public DomainException(String message, Throwable cause) {
super(message, cause);
}
}
// Конкретные исключения
public class InsufficientFundsException extends DomainException {
private final BigDecimal required;
private final BigDecimal available;
public InsufficientFundsException(BigDecimal required, BigDecimal available) {
super(String.format("Insufficient funds. Required: %s, Available: %s",
required, available));
this.required = required;
this.available = available;
}
public BigDecimal getRequired() { return required; }
public BigDecimal getAvailable() { return available; }
}
public class PaymentAlreadyProcessedException extends DomainException {
private final String paymentId;
public PaymentAlreadyProcessedException(String paymentId) {
super("Payment " + paymentId + " has already been processed");
this.paymentId = paymentId;
}
}
public class InvalidTransactionStateException extends DomainException {
private final String currentState;
private final String attemptedAction;
public InvalidTransactionStateException(String currentState, String attemptedAction) {
super(String.format("Cannot %s transaction in %s state",
attemptedAction, currentState));
this.currentState = currentState;
this.attemptedAction = attemptedAction;
}
}
2. Сервисный слой (Application Layer)
Сервисы выбрасывали исключения и документировали их:
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentRepository repository;
private final AccountService accountService;
private final AuditLogger auditLogger;
/**
* Обрабатывает платёж
* @param paymentRequest запрос на платёж
* @return результат обработки
* @throws InsufficientFundsException если нет средств
* @throws PaymentAlreadyProcessedException если уже обработан
*/
public PaymentResult processPayment(PaymentRequest paymentRequest) {
// Логика с несколькими проверками
Payment payment = repository.findById(paymentRequest.getPaymentId())
.orElseThrow(() -> new PaymentNotFoundException(
paymentRequest.getPaymentId()));
// Проверка статуса
if (payment.isProcessed()) {
throw new PaymentAlreadyProcessedException(
paymentRequest.getPaymentId());
}
// Проверка баланса
Account account = accountService.getAccount(payment.getAccountId());
if (account.getBalance().compareTo(payment.getAmount()) < 0) {
throw new InsufficientFundsException(
payment.getAmount(),
account.getBalance());
}
try {
// Обработка платежа
payment.process();
repository.save(payment);
// Логирование успеха
auditLogger.log("Payment processed", payment.getId());
return new PaymentResult(payment.getId(), "SUCCESS");
} catch (DatabaseException ex) {
// Пробросить выше, не обрабатывать здесь
auditLogger.logError("DB error during payment", payment.getId(), ex);
throw ex;
} catch (Exception ex) {
// Неожиданное исключение
auditLogger.logError("Unexpected error", payment.getId(), ex);
throw new PaymentProcessingException(
"Failed to process payment", ex);
}
}
}
3. REST контроллер (Presentation Layer)
Контроллеры обрабатывали исключения и преобразовывали в HTTP ответы:
@RestController
@RequestMapping("/api/payments")
@RequiredArgsConstructor
public class PaymentController {
private final PaymentService paymentService;
private final PaymentMapper mapper;
@PostMapping
public ResponseEntity<?> processPayment(
@Valid @RequestBody PaymentRequest request) {
try {
PaymentResult result = paymentService.processPayment(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(mapper.toResponse(result));
} catch (InsufficientFundsException ex) {
// 402 Payment Required
ErrorResponse error = ErrorResponse.builder()
.code("INSUFFICIENT_FUNDS")
.message(ex.getMessage())
.timestamp(Instant.now())
.details(Map.of(
"required", ex.getRequired(),
"available", ex.getAvailable()
))
.build();
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
.body(error);
} catch (PaymentAlreadyProcessedException ex) {
// 409 Conflict
ErrorResponse error = ErrorResponse.builder()
.code("ALREADY_PROCESSED")
.message(ex.getMessage())
.timestamp(Instant.now())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(error);
} catch (DomainException ex) {
// 400 Bad Request для прочих доменных ошибок
ErrorResponse error = ErrorResponse.builder()
.code("VALIDATION_ERROR")
.message(ex.getMessage())
.timestamp(Instant.now())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(error);
}
}
}
4. Global Exception Handler
Мы использовали @ControllerAdvice для централизованной обработки:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Доменные исключения (бизнес-логика)
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<ErrorResponse> handleInsufficientFunds(
InsufficientFundsException ex, HttpServletRequest request) {
log.warn("Insufficient funds error: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
.body(ErrorResponse.builder()
.code("INSUFFICIENT_FUNDS")
.message(ex.getMessage())
.path(request.getRequestURI())
.timestamp(Instant.now())
.build());
}
// Ошибки валидации
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex, HttpServletRequest request) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
log.warn("Validation error: {}", errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse.builder()
.code("VALIDATION_ERROR")
.message("Input validation failed")
.errors(errors)
.timestamp(Instant.now())
.path(request.getRequestURI())
.build());
}
// Неожиданные ошибки (логируем как ошибку)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(
Exception ex, HttpServletRequest request) {
String traceId = UUID.randomUUID().toString();
log.error("Unexpected error [trace: {}]", traceId, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.builder()
.code("INTERNAL_ERROR")
.message("An unexpected error occurred")
.traceId(traceId) // Для отслеживания в логах
.timestamp(Instant.now())
.path(request.getRequestURI())
.build());
}
}
Структура ErrorResponse
@Data
@Builder
public class ErrorResponse {
private String code; // Код ошибки (INSUFFICIENT_FUNDS)
private String message; // Сообщение об ошибке
private Instant timestamp; // Когда произошла ошибка
private String path; // Какой путь вызвал ошибку
private String traceId; // Для отслеживания в логах
private List<String> errors; // Дополнительные ошибки (валидация)
private Map<String, Object> details; // Доп. информация
}
Логирование ошибок
@Component
@Slf4j
public class ExceptionLogger {
/**
* Логируем только то, что нужно
*/
public void logDomainException(DomainException ex, String context) {
// Доменное исключение — обычная ситуация (log.warn)
log.warn("Domain error in {}: {}", context, ex.getMessage());
}
public void logTechnicalException(Exception ex, String context) {
// Техническая ошибка (БД, сеть и т.д.) — ошибка (log.error)
log.error("Technical error in {}: {}", context, ex.getMessage(), ex);
}
public void logUnexpectedException(Exception ex, String context, String traceId) {
// Непредвиденная ошибка — критическая (log.error с трассировкой)
log.error("UNEXPECTED ERROR [traceId={}] in {}", traceId, context, ex);
}
}
Правила обработки исключений
1. **Не подавляй (don't swallow)**
❌ try { ... } catch (Exception ex) { } // Ошибка исчезла!
✅ Либо пробрось выше, либо логируй и оборачивай
2. **Логируй один раз на уровне**
❌ log.error(...) в методе
...выбрасываем исключение...
log.error(...) в обработчике
Логируется ДВА РАЗА!
✅ Выбрасываем исключение один раз, логируем один раз
3. **Используй свои исключения для бизнес-логики**
✅ throw new InsufficientFundsException(...)
❌ throw new IllegalArgumentException(...)
4. **Оборачивай низкоуровневые ошибки в свои исключения**
✅ } catch (SQLException ex) {
throw new DatabaseException("Failed to fetch user", ex);
}
❌ throw new RuntimeException(ex);
5. **Предоставляй контекст в исключении**
✅ new PaymentProcessingException(
"Payment " + paymentId + " failed", cause);
❌ throw new Exception("Error");
На практике: обработка транзакций
@Transactional
public void transferMoney(String fromAccountId, String toAccountId,
BigDecimal amount) {
try {
Account from = accountService.lockAccount(fromAccountId);
Account to = accountService.lockAccount(toAccountId);
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException(amount, from.getBalance());
}
from.debit(amount);
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
auditLogger.log("Transfer completed", fromAccountId, toAccountId, amount);
} catch (InsufficientFundsException ex) {
// Spring откатит транзакцию автоматически (@Transactional)
throw ex; // Пробрасываем для обработки выше
} catch (DatabaseException ex) {
// БД ошибка — откат транзакции
auditLogger.logError("Database error during transfer", ex);
throw new TransferFailedException("Database error", ex);
} catch (Exception ex) {
// Неожиданная ошибка — откат и логирование
auditLogger.logError("Unexpected error during transfer", ex);
throw new TransferFailedException("Unexpected error", ex);
}
}
Итого: мой подход к обработке исключений
- Иерархия: DomainException -> конкретные исключения (InsufficientFundsException и т.д.)
- Слои: доменные ошибки -> сервисный слой -> контроллер -> обработчик -> HTTP ответ
- Маппирование: каждое исключение -> HTTP код (400, 402, 409 и т.д.)
- Логирование: один раз на уровне обработки (не в каждом слое)
- Контекст: сохраняем информацию об ошибке (ID, сумму, состояние и т.д.)
- Отслеживание: traceId для отслеживания в логах
Это позволило нам:
- Давать клиентам понятные ошибки
- Легко отладить проблемы в продакшене
- Разделить бизнес-логику от технических ошибок
- Избежать дублирования кода обработки исключений