Как настроить rollback для checked exceptions?
По умолчанию Spring не откатывает транзакцию при checked exceptions (наследники Exception, но не RuntimeException). Чтобы это исправить, используйте параметр rollbackFor.
🟢 Junior Level
По умолчанию Spring не откатывает транзакцию при checked exceptions (наследники Exception, но не RuntimeException). Чтобы это исправить, используйте параметр rollbackFor.
Самый простой способ
@Transactional(rollbackFor = Exception.class)
public void businessMethod() throws MyCheckedException {
// Теперь ЛЮБОЕ исключение вызовет rollback
}
// rollbackFor говорит TransactionInterceptor: «это исключение тоже считай // сигналом к откату». Проверка: если exception instanceof rollbackFor → setRollbackOnly().
Аналогия
Представьте fire alarm: по умолчанию срабатывает только на пожар (RuntimeException). Checked exception — это утечка газа, которую тоже нужно обработать. rollbackFor говорит системе: “любая авария = эвакуация”.
Для конкретного исключения
@Transactional(rollbackFor = IOException.class)
public void readFile() throws IOException {
// IOException вызовет rollback
}
Для нескольких исключений
@Transactional(rollbackFor = {IOException.class, MyBusinessException.class})
public void process() throws IOException, MyBusinessException {
// Оба исключения вызовут rollback
}
Золотое правило
Если ваше бизнес-исключение означает, что операция не может быть завершена успешно — оно должно вызывать откат. Используйте rollbackFor = Exception.class для безопасности.
Когда НЕ стоит менять rollback-правила
- Если exception = сигнал о баге (NPE, IAE) — дефолтный rollback правилен
- Если нет clear правила в команде — разные rollbackFor в разных сервисах создают хаос
- Если используете @ControllerAdvice — пусть exception долетит до контроллера, а там решите, что возвращать клиенту
🟡 Middle Level
Почему это важно
В реальных проектах сервисы часто используют кастомные бизнес-исключения. Если они наследуются от Exception (Checked), Spring по умолчанию сделает COMMIT при их возникновении. Это может оставить базу в несогласованном состоянии.
// ПЛОХО — InsufficientFundsException extends Exception
@Transactional
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
// Spring сделает COMMIT несмотря на исключение!
}
Три подхода к решению
1. rollbackFor (рекомендуется для checked)
@Transactional(rollbackFor = Exception.class)
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
// Rollback для всех исключений
}
2. Перейти на RuntimeException
// InsufficientFundsException теперь extends RuntimeException
@Transactional
public void withdraw(BigDecimal amount) {
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException(); // Rollback автоматически
}
}
3. Программный rollback
@Transactional
public void withdraw(BigDecimal amount) {
try {
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
} catch (InsufficientFundsException e) {
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
throw e;
}
}
Распространённые ошибки
| Ошибка | Что происходит | Как исправить |
|---|---|---|
Забыть rollbackFor |
COMMIT при checked exception | rollbackFor = Exception.class |
Опечатка в rollbackForClassName |
Правило молча не срабатывает | Использовать rollbackFor с Class literal |
Conflicting rollbackFor + noRollbackFor |
Непредсказуемое поведение | Однозначные правила |
Catch без setRollbackOnly() |
Exception пойман → COMMIT | Вызвать setRollbackOnly() в catch |
Сравнение подходов
| Подход | Плюсы | Минусы | Когда использовать |
|---|---|---|---|
rollbackFor = Exception.class |
Просто, надёжно | Rollback даже для “не-ошибок” | Стандартный подход |
rollbackFor = {SpecificException.class} |
Точный контроль | Нужно перечислить все | Когда некоторые checked exception = не ошибка |
| Переход на RuntimeException | Автоматический rollback, меньше boilerplate | Требует рефакторинг exception hierarchy | Новый проект или рефакторинг |
Программный setRollbackOnly() |
Условный rollback | Boilerplate, Spring coupling | Сложная бизнес-логика |
Когда НЕ использовать rollback для checked exceptions
| Ситуация | Почему | Альтернатива |
|---|---|---|
| Бизнес-валидация | Ожидаемая ситуация, данные валидны | noRollbackFor = ValidationException.class |
| Graceful degradation | Система должна работать | Не exception, а result object |
| Partial success | Часть данных корректна | Обработать, commit, log error |
🔴 Senior Level
rollbackFor Attribute Resolution
Spring резолвит rollback rules через priority chain:
// AbstractFallbackTransactionAttributeSource
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) {
for (TransactionAnnotationParser parser : annotationParsers) {
TransactionAttribute attr = parser.parseTransactionAnnotation(ae);
if (attr != null) return attr;
}
return null;
}
// SpringTransactionAnnotationParser
public TransactionAttribute parseTransactionAnnotation(Transactional ann) {
RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
Class<? extends Throwable>[] rollbackFor = ann.rollbackFor();
for (Class<? extends Throwable> ex : rollbackFor) {
rbta.getRollbackRules().add(new RollbackRuleAttribute(ex));
}
String[] rollbackForClassName = ann.rollbackForClassName();
for (String ex : rollbackForClassName) {
rbta.getRollbackRules().add(new RollbackRuleAttribute(ex));
}
Class<? extends Throwable>[] noRollbackFor = ann.noRollbackFor();
for (Class<? extends Throwable> ex : noRollbackFor) {
rbta.getRollbackRules().add(new NoRollbackRuleAttribute(ex));
}
return rbta;
}
String-Based Rollback Rules — Hidden Danger
@Transactional(rollbackForClassName = {"MyBusinessException"})
public void process() { ... }
Spring резолвит class name в рантайме:
public RollbackRuleAttribute(String exceptionPattern) {
this.exceptionName = exceptionPattern;
// NOT validated at startup — resolved lazily during exception handling
}
Risk: Опечатка → правило молча не сработает → нет rollback. Используйте rollbackFor с Class literal для type safety.
Combining rollbackFor и noRollbackFor
@Transactional(
rollbackFor = Exception.class,
noRollbackFor = {NotFoundException.class, ValidationException.class}
)
public void process() {
// Exception → rollback
// NotFoundException → NO rollback (commit despite exception)
// ValidationException → NO rollback
}
Rule matching order:
- Наиболее специфичный match побеждает (deepest in hierarchy)
- При одинаковой глубине — первое совпавшее правило
- Default rules (RuntimeException, Error) проверяются последними
// noRollbackFor = «это RuntimeException, но НЕ откатывай». Полезно для // ожидаемых бизнес-исключений, которые наследуются от RuntimeException.
Программный Rollback — Когда и Зачем
Scenario 1: Checked exception без propagation
@Transactional
public void processFiles(List<File> files) {
for (File file : files) {
try {
processFile(file); // throws IOException
} catch (IOException e) {
log.error("File {} failed", file.getName());
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
break; // Continue processing, but mark for rollback
}
}
}
Scenario 2: Условный rollback
@Transactional
public void transferWithLimit(Long from, Long to, BigDecimal amount) {
if (amount.compareTo(DAILY_LIMIT) > 0) {
log.warn("Exceeds daily limit");
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
return; // Method returns normally, but transaction rolls back
}
// ... transfer logic
}
Edge Cases (минимум 3)
-
String typo в
rollbackForClassName:@Transactional(rollbackForClassName = "BusinesException")— silently ignored. Spring не валидирует class name при инициализации. -
Conflicting rules:
rollbackFor = Exception.class+noRollbackFor = RuntimeException.class. Специфичность определяет победителя. RuntimeException глубже →noRollbackForпобеждает → нет rollback для RuntimeException, есть для checked. -
AOP ordering: Custom exception handler aspect может перехватить exception до
TransactionInterceptor. Interceptor видит normal return → COMMIT. Решение:@Orderдля контроля порядка. -
Self-invocation bypass: Вызов
this.method()внутри класса — proxy не участвует, rollback rules не применяются вообще. Нет транзакции = нет rollback. -
Async методы:
@Asyncс checked exceptions — exception не прокидывается в caller’s transaction context. Caller продолжает COMMIT. Future содержит exception — нужноfuture.get().
Global Rollback Configuration
@Configuration
public class TransactionConfig {
@Bean
public TransactionInterceptor transactionInterceptor(
PlatformTransactionManager tm) {
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionManager(tm);
RuleBasedTransactionAttribute defaultRule =
new RuleBasedTransactionAttribute();
defaultRule.setRollbackRules(
List.of(new RollbackRuleAttribute(Exception.class)));
NameMatchTransactionAttributeSource source =
new NameMatchTransactionAttributeSource();
source.addTransactionalMethod("*", defaultRule);
interceptor.setTransactionAttributeSource(source);
return interceptor;
}
}
Migration Strategy: From Checked to Unchecked
// Step 1: Добавить rollbackFor к существующим методам
@Transactional(rollbackFor = Exception.class)
public void oldMethod() throws BusinessException { ... }
// Step 2: Изменить иерархию exception
public class BusinessException extends RuntimeException { ... } // Was extends Exception
// Step 3: Убрать throws и rollbackFor
@Transactional
public void newMethod() { ... } // Clean
// Step 4: Обновить callers (больше не нужен try-catch)
service.newMethod(); // Вместо: try { service.oldMethod(); } catch...
Performance Numbers
| Операция | Время |
|---|---|
| Rollback rule resolution (cached) | ~0.5 μs |
rollbackFor class matching |
~0.1 μs |
| String-based resolution | ~5-10 μs (class loading) |
| Actual DB rollback | 1-5 ms (PostgreSQL), 5-50 ms (InnoDB) |
Memory Implications
RollbackRuleAttributeобъекты — ~100 bytes each, хранятся вTransactionAttribute- String-based rules: Class загрузка в runtime — memory overhead если exception class большой
- Exception stack traces — могут быть большими. При frequent rollbacks = GC pressure.
Thread Safety
Rollback rules immutable — thread-safe. ThreadLocal state в TransactionSynchronizationManager — конкурентные вызовы изолированы.
Production War Story
Финансовый сервис: withdraw() бросал InsufficientFundsException extends Exception. @Transactional без rollbackFor. При insufficient funds exception — транзакция делала COMMIT, баланс списывался, но перевод не завершался. Деньги пропали. Fix: rollbackFor = Exception.class + миграция на RuntimeException hierarchy.
Monitoring
Debug logging:
logging:
level:
org.springframework.transaction.interceptor: TRACE
Micrometer:
@Bean
public TransactionSynchronization transactionMetrics(MeterRegistry registry) {
return new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
registry.counter("transaction.rollback").increment();
}
}
};
}
Highload Best Practices
- Baseline:
rollbackFor = Exception.classна всех write-методах. Без исключений. - Минимизация checked exceptions: Перевести бизнес-исключения на RuntimeException. Aligns with Spring philosophy.
- Global configuration: Использовать
TransactionInterceptorс глобальными rollback rules вместо дублирования аннотаций. - Never use
rollbackForClassName: String-based rules — silent failure risk. Use Class literals. - Test rollback: Integration tests должны проверять что rollback действительно происходит для каждого exception type.
- Monitor rollback rate: Высокий % rollback = проблема в валидации/бизнес-логике.
🎯 Шпаргалка для интервью
Обязательно знать:
- rollbackFor = Exception.class — rollback для всех исключений, рекомендуемый baseline
- rollbackFor = {SpecificException.class} — точный контроль для конкретных типов
- Программный rollback: TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
- rollbackForClassName (string) — risky: опечатка молча игнорируется, используйте Class literals
- noRollbackFor исключает specific исключения из rollback (например, NotFoundException)
- Rule matching order: наиболее специфичный match (deepest in hierarchy) побеждает
Частые уточняющие вопросы:
- Три подхода к checked exception rollback? — rollbackFor, переход на RuntimeException, программный setRollbackOnly
- Почему rollbackForClassName опасен? — String resolution lazily, typo не валидируется при старте
- Когда использовать программный rollback? — Условный rollback, complex business rules
- Global rollback configuration — как? — TransactionInterceptor с RuleBasedTransactionAttribute
Красные флаги (НЕ говорить):
- “rollbackForClassName = безопасный способ” — silent failure при опечатке
- “Catch exception = automatic rollback” — Spring не видит caught exceptions
- “Можно смешивать rollbackFor и noRollbackFor бездумно” — conflicting rules создают непредсказуемое поведение
Связанные темы:
- [[19. Какие исключения по умолчанию вызывают rollback]]
- [[18. Что такое rollback в транзакциях]]
- [[16. Что такое аннотация @Transactional]]