Strange behaviour with @Transactional(propagation=Propagation.REQUIRES_NEW)

23,976

Spring transactions, by default, work by wrapping the Spring bean with a proxy which handles the transaction and the exceptions. When you call method2() from method1(), you're completely bypassing this proxy, so it can't start a new transaction, and you're effectively calling method2() from the same transaction as the one opened by the call to method1().

On the contrary, when you call a method of another injected bean from method1(), you're in fact calling a method on a transactional proxy. So if this alien method is marked with REQUIRES_NEW, a new transaction is started by the proxy, and you're able to catch the exception in method1() and resume the outer transaction.

This is described in the documentation.

Share:
23,976
DessDess
Author by

DessDess

SOreadytohelp

Updated on July 05, 2022

Comments

  • DessDess
    DessDess almost 2 years

    Here is my problem :

    I'm running a batch on a Java EE/Spring/Hibernate application. This batch calls a method1. This method calls a method2 which can throw UserException (a class extending RuntimeException). Here is how it looks like :

    @Transactional
    public class BatchService implements IBatchService {
     @Transactional(propagation=Propagation.REQUIRES_NEW)
     public User method2(User user) {
       // Processing, which can throw a RuntimeException
     }
    
     public void method1() {
       // ...
       try {
         this.method2(user);
       } catch (UserException e) {
         // ...
       }
       // ...
     }
    }
    

    The exception is catched as the execution continues, but at the end of method1 when the transaction is closed a RollbackException is thrown.

    Here is the stack trace :

    org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
        at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:476)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy128.method1(Unknown Source)
    at batch.BatchController.method1(BatchController.java:202)
    

    When method2 is not throwing this exception, it works well.

    What I have tried:

    • Setting @Transactional(noRollbackFor={UserException.class})) on method1
    • Try and catch in method2

    But it didn't change anything.

    As the exception is thrown in a different transaction where a rollback happened I don't understand why it doesn't work. I had a look at this : Jpa transaction javax.persistence.RollbackException: Transaction marked as rollbackOnly but it didn't really help me.

    I will be very greatful if someone could give me a clue.

    Update

    I've made it work by setting propagation=Propagation.REQUIRES_NEW on the method called by method2 (which is actually the one which is sending the exception). This method is defined in a class very similar to my BatchService. So I don't see why it works on this level and not on method2.

    • I've set method2 as public as the annotation @Transactional is not taken into account if the method is private as said in the documentation :

    The @Transactional annotation may be placed before an interface definition, a method on an interface, a class definition, or a public method on a class.

    • I also tried to use Exception instead of RuntimeException (as it is more appropriate) but it also didn't change anything.

    Even if it is working the question remains open as it has a strange behaviour and I would like to understand why it's not acting like it should be.

  • DessDess
    DessDess about 11 years
    Perfect answer with a real understanding of what is happening, Thank you !
  • Wim Deblauwe
    Wim Deblauwe about 9 years
    Note that if you need these self invocations to work, you should use AspectJ mode in Spring. See docs.spring.io/spring/docs/current/spring-framework-referenc‌​e/…: "Consider the use of AspectJ mode (see mode attribute in table below) if you expect self-invocations to be wrapped with transactions as well. In this case, there will not be a proxy in the first place; instead, the target class will be weaved (that is, its byte code will be modified) in order to turn @Transactional into runtime behavior on any kind of method."
  • Vito
    Vito over 8 years
    yeah,real understanding of the spring transaction... thank you