Як налаштувати rollback для checked винятків?
За замовчуванням 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().
Аналогія
Уявіть пожежну сигналізацію: за замовчуванням спрацьовує тільки на пожежу (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 винятків
| Ситуація | Чому | Альтернатива |
|---|---|---|
| Бізнес-валідація | Очікувана ситуація, дані валідні | 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:
- Найбільш специфічний збіг перемагає (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: найбільш специфічний збіг (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]]