Java Quickies – What you wish you knew about Spring @Transactional annotation

This article is about some special cases which can be faced using the @Transactional annotation. This article is here to warn you about some features you may not know.

When is Spring triggering a rollback using @Transactional?

The answer everyone wants to say is: whenever an exception is thrown.

  @Transactional
  public Long create(User user) {
    userDao.create(user);
    throw new RuntimeException();
  }

If I execute the code, everyone knows that the RuntimeException will trigger a rollback, consequently my user will not be created. Now, what about this piece of code:

  @Transactional
  public Long create(User user) throws MyCheckedException {
    userDao.create(user);
    throw new MyCheckedException();
  }

Here, MyCheckedException extends Exception making it a checked exception as opposed as the exceptions extending RuntimeException. Looking at this code, I do not expect a different scenario from the first one. However Spring states that a rollback will occur if a RuntimeException is thrown, but will not happen if it is a checked exception! Thus, when exiting my create method, there will be a commit and my user will be persisted, even though I never caught the exception!
Well, you really just need to be aware of this behavior as Spring can help you fix this issue. The easiest is, if we want things to rollback in the particular method but not in the whole application.

  @Transactional(rollbackFor = MyCheckedException.class)
  public Long create(User user) throws MyCheckedException {
    userDao.create(user);
    throw new MyCheckedException();
  }

Quite easy, hm? Now let’s imagine we would like to rollback on every MyCheckedException thrown. I found the solution in this StackOverflow post. The first answer, and the one being accepted is not very interesting as it forces us to use a different annotation than @Transaction. The second answer shows a way to solve the issue and still use the @Transactional annotation. Here is the Spring configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="
	  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
      http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
      
	<jee:jndi-lookup id="dataSource" jndi-name="jdbc/resourcePool" />
	
	<aop:config/>

	<bean id="transactionManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<bean id="txAttributeSource" class="io.resourcepool.dao.core.RollbackForTransactionAttributeSource" />

	<bean id="txInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
	    <property name="transactionManagerBeanName" value="transactionManager" />
	    <property name="transactionAttributeSource" ref="txAttributeSource" />
	</bean>
	
	<bean class="org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor">
	    <property name="transactionAttributeSource" ref="txAttributeSource" />
	    <property name="adviceBeanName" value="txInterceptor" />
	</bean>
</beans>

We alose need this class:

package io.resourcepool.dao.core;

import java.lang.reflect.AnnotatedElement;

import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.DelegatingTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;

import io.resourcepool.exception.MyCheckedException;

public class RollbackForTransactionAttributeSource extends AnnotationTransactionAttributeSource {
  @Override
  protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) {
    TransactionAttribute target = super.determineTransactionAttribute(ae);
    if (target == null) {
      return null;
    }
    return new DelegatingTransactionAttribute(target) {
      @Override
      public boolean rollbackOn(Throwable ex) {
        return ex instanceof RuntimeException || ex instanceof MyCheckedException;
      }
    };
  }
}

For this to work, you need to remove the <tx:annotation-driven /> from your configuration. Actually, we do not even need the tx schema. However, the aop is mandatory. The magic happens next in our class. We will be able to select which exceptions will trigger a rollback and which will not. If you use the second way, please be sure to add the rollback on RuntimeException. Indeed, if you do not set it, even RuntimeException will not trigger a rollback. This is a big different with the rollbackFor way. The rollbackFor declaration will trigger a rollback on RuntimeException even if you do not explicitly state it.

Another @Transactional weird thing: the UnexpectedRollbackException

Here is the second special case I faced during a batch: the UnexpectedRollbackException! You will hate this exception as it will throw when the transaction will try to commit whereas the problem had occurred way earlier.

First of all, you need to be clear about how the Spring @Transactional annotation is working. Here is the schema from the Spring documentation:

tx

For each public method annotated with @Transactional, Spring will create an AOP proxy which goal is to start a transaction before entering the method body and commit (or rollback) it, just after exiting the method.

Let’s get back to our problem and see how you can trigger an UnexpectedRollbackException:

public class MyService1 {
  @Autowired
  private MyService2 service2;

  @Autowired
  private UserDao userDao;

  @Transactional
  public void create(User user) {
    try {
      service2.validateAddress(user.getAddress());
    } catch (IllegalArgumentException ex) {
      user.setAddress(null);
    }
    userDao.create(user);
  }
}

class MyService2 {
  @Transactional
  public void validateAddress(Address address) {
    // Some validations using dao
    if (!isValid) {
      throws new IllegalArgumentException();
    }
  }
}

Let’s imagine the weird case where before creating a user, we need to validate his address using another service. If the validation fails, we will just set the address to null. The validateAddress method is annotated with @Transaction, as we saw, if the validation fails, the IllegalArgumentException (it extends RuntimeException) will trigger a rollback. We are aware of this behavior, so we will catch this exception in our create method. We will then call our create method from the dao. The problem will occur when we will try to commit this transaction. Indeed, Spring knows a part of the transaction had to rollback, Spring will not let you commit, and will throw the UnexpectedRollbackException to let you know something went wrong during this transaction!

Our example was a very easy case. In a real life application, finding the cause of this exception can get very tricky. You can still try to get more information using the getMostSpecificCause() and getRootCause() methods of the UnexpectedRollbackException. As far as I am concerned, I started my batch in debug having put some breakpoints directly inside the rollback method of the Spring Transaction Manager. The stacktrace showed my exactly what threw the exception.

Once the guilty has been caught, there is not many way to fix it. Whether we can get rid on the exception, or we need to add it to the noRollbackFor list:

class MyService2 {
  @Transactional(noRollbackFor = IllegalArgumentException.class)
  public void validateAddress(Address address) {
    // Some validations using dao
    if (!isValid) {
      throws new IllegalArgumentException();
    }
  }
}

The exception will still be thrown, but will not trigger a rollback. Thus, the commit will have no excuse to fail 😉

5 Thoughts on Java Quickies – What you wish you knew about Spring @Transactional annotation

  1. hi,

    I am hit with exactly the same problem of not rolling back Spring transaction on checked exception. I could not understand this line from above Could you please clarify?

    For this to work, you need to remove the from your configuration… (What we should remove from the configuration)

    Thanks
    Ram

  2. the second question cased by the nested transaction. spring transaction annotation supports it by using Propagation.

    so is it a WEIRD THING?

    1. I do not say something is wrong with spring transaction annotation. This is just that you get this exception away from where the issue actually occured, and it may look weird. Well it did for me!

  3. I’ve been hit by the “unexpected UnexpectedRollbackException” too recently. Nice to find this write-up.

    IMHO Spring should not mark the transaction for rollback, when MyService2#validateAddress() throws an exception: It is just taking part in the current transaction and should not decide this.
    If the exception is not caught and bubbles up through MyService1#create(), the transaction could be rolled back soon enough.
    Perhaps this can somehow be configured in Spring?

    Many thanks

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.