Affects: 5.2.3.RELEASE

I have replicated the issue with 5.2.3.RELEASE, but I believe later versions are also affected.


What I'm trying to do

I need a transaction which will not be rolled back if a RuntimeException is thrown, but I need to do this manually (i.e. using a TransactionTemplate) rather than using the @Transactional annotation.

In summary, I am trying to achieve this behaviour...

@Transactional(dontRollbackOn = RuntimeException.class)
public void myMethod() { }

... using a TransactionTemplate.


Issue description:

I have tried to achieve the behaviour outlined above by defining a new NoRollbackRuleAttributeand constructing a TransactionTemplate with it:

RuleBasedTransactionAttribute rules = new RuleBasedTransactionAttribute();
rules.getRollbackRules().add(new NoRollbackRuleAttribute(RuntimeException.class));

TransactionTemplate transactionTemplateNoRollback = new TransactionTemplate(platformTransactionManager,rules);

CustomObject = transactionTemplateNoRollback.execute(
          (TransactionCallback<CustomObject>) status -> {
            throw new RuntimeException();
});

The NoRollbackRuleAttributeseems to be ignored and an UnexpectedRollbackException is thrown if my method my method throws any RuntimeException:

org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

(Possible) cause

The TransactionTemplate(PlatformTransactionManager transactionManager, TransactionDefinition transactionDefinition) constructor invokes the DefaultTransactionDefinition(TransactionDefinition other) constructor which looks as follows:

public DefaultTransactionDefinition(TransactionDefinition other) {
        this.propagationBehavior = other.getPropagationBehavior();
        this.isolationLevel = other.getIsolationLevel();
        this.timeout = other.getTimeout();
        this.readOnly = other.isReadOnly();
        this.name = other.getName();
    }

Neither constructor seems to do anything with the RollbackRules provided to it.


*** Work around***

I have fallen back to using @Transactional(dontRollbackOn = RuntimeException.class) for now, but that requires me to create a new public method within a different class which is exactly what I was trying to avoid.

Comment From: joeltoby

If this behaviour is correct / to be expected, is there another way to achieve the desired behaviour short of applying a custom TransactionInterceptor with AOP magic?

Comment From: jhoeller

This is by design: TransactionTemplate does not support custom rollback rules. Technically TransactionTemplate isn't even aware of TransactionAttribute since that variant is an extended definition only supported by TransactionInterceptor (and therefore also living in the latter's package). The template operates on plain TransactionDefinition and uses fixed rollback behavior for all exceptions thrown from its callbacks, effectively RuntimeExceptions and Errors.

Alternatively, you may use the PlatformTransactionManager directly: Inject it, call getTransaction on it, perform some resource operations, then call commit in a finally block... and handle potential runtime exceptions any way you need to, without calling rollback. You may also selectively handle the outcome, as long as you eventually call commit (or rollback, for that matter) to complete the transaction.

Comment From: joeltoby

Thanks for the helpful and quick reply Juergen.

Comment From: Jiiiiiin

Spring NoRollbackRuleAttributes ignored by TransactionTemplate

可以尝试做一个容器在 tmpl 模版之外来接受正常/异常,从而实现 异常的区别处理。