Вопрос 20 · Раздел 11

Как настроить rollback для checked exceptions?

По умолчанию Spring не откатывает транзакцию при checked exceptions (наследники Exception, но не RuntimeException). Чтобы это исправить, используйте параметр rollbackFor.

Версии по языкам: English Russian Ukrainian

🟢 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-правила

  1. Если exception = сигнал о баге (NPE, IAE) — дефолтный rollback правилен
  2. Если нет clear правила в команде — разные rollbackFor в разных сервисах создают хаос
  3. Если используете @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:

  1. Наиболее специфичный match побеждает (deepest in hierarchy)
  2. При одинаковой глубине — первое совпавшее правило
  3. 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)

  1. String typo в rollbackForClassName: @Transactional(rollbackForClassName = "BusinesException") — silently ignored. Spring не валидирует class name при инициализации.

  2. Conflicting rules: rollbackFor = Exception.class + noRollbackFor = RuntimeException.class. Специфичность определяет победителя. RuntimeException глубже → noRollbackFor побеждает → нет rollback для RuntimeException, есть для checked.

  3. AOP ordering: Custom exception handler aspect может перехватить exception до TransactionInterceptor. Interceptor видит normal return → COMMIT. Решение: @Order для контроля порядка.

  4. Self-invocation bypass: Вызов this.method() внутри класса — proxy не участвует, rollback rules не применяются вообще. Нет транзакции = нет rollback.

  5. 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

  1. Baseline: rollbackFor = Exception.class на всех write-методах. Без исключений.
  2. Минимизация checked exceptions: Перевести бизнес-исключения на RuntimeException. Aligns with Spring philosophy.
  3. Global configuration: Использовать TransactionInterceptor с глобальными rollback rules вместо дублирования аннотаций.
  4. Never use rollbackForClassName: String-based rules — silent failure risk. Use Class literals.
  5. Test rollback: Integration tests должны проверять что rollback действительно происходит для каждого exception type.
  6. 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]]