Thursday, April 8, 2010

Spring Transactions - Sample Applications

Introduction
A while back I created four simple Java applications demonstrating Spring's transaction management implementation and recently thought it would be nice to share them. They may be too simple for most people but for someone new to Spring they may actually be helpful.

If you want to learn about transaction management in Spring then a good place to start is the Spring documentation and for the running examples you can download the spring-transactions-samples.zip Eclipse archive file. The four sample applications are broken down into four of Spring's transaction management abstractions, i.e.:
  • Declarative
    • XML
    • Annotations
  • Programmatic
    • TransactionTemplate
    • PlatformTransactionManager

In the examples that follow I have used the Spring DataSourceTransactionManager implementation as it is linked to a JDBC DataSource. I am using an in-memory database provided by HSQLDB and therefore don't have to do much except define configuration settings for the datasource in the Spring bean configuration file: 

transaction-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
 <bean id="testDataSource" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
  <property name="url" value="jdbc:hsqldb:testdb" />
  <property name="username" value="sa" />
  <property name="password" value="" />
 </bean>
 
 <bean id="transactionManager"
  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="testDataSource" />
 </bean>

</beans>

Declarative - XML
Declarative transaction management is the most common Spring implementation as it has the least impact on application code. The XML declarative approach configures the transaction attributes in a Spring bean configuration file. The sample application for this implementation can be found in the com.mydomain.spring.transaction.declarative.xml package. There are a few simple methods with different transaction settings configured to demonstrate some of the possible scenarios. I recommend running the sample application and viewing the logs to see what Spring prints out. 

xml-test.xml
<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

 <import resource="classpath:transaction-test.xml" />
 
 <bean id="transactionTestService" class="com.mydomain.spring.transaction.declarative.xml.XmlTransactionTestService" />

 <tx:advice id="txAdvice" transaction-manager="transactionManager">
  <tx:attributes>
   <tx:method name="readOnly*" read-only="true" />
   <tx:method name="*" />
  </tx:attributes>
 </tx:advice>

 <aop:config>
  <aop:pointcut id="xmlTransactionTestServiceOperation"
   expression="execution(* com.mydomain.spring.transaction.declarative.xml.XmlTransactionTestService.*(..))" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="xmlTransactionTestServiceOperation" />
 </aop:config>
</beans>

The <tx:advice/> definition makes the service object TransactionTestService bean transactional and defines all methods starting with 'readOnly' to execute in the context of a read-only transaction, and all other methods to execute with the default transaction semantics.

The <aop:config/> definition ensures that the transactional advice defined by the txAdvice bean executes at the appropriate points in the program.

The above configuration will be used to create a transactional proxy around the object that is created from the transactionTestService bean definition. The proxy will be configured with the transactional advice, so that when an appropriate method is invoked on the proxy, a transaction is started, suspended, marked as read-only, and so on, depending on the transaction configuration associated.

XmlTransactionTestService.java
public class XmlTransactionTestService implements TransactionTestService {
 private final static Log log = LogFactory.getLog(XmlTransactionTestService.class);
 
 public void readOnlyCommitExampleTransaction() {
  log.info("-- XmlTransactionTestService.readOnlyCommitExampleTransaction -- expects transaction commit and read-only");
  // do stuff
 }
 
 public void readOnlyRollbackExampleTransaction() {
  log.info("-- XmlTransactionTestService.readOnlyRollbackExampleTransaction -- expects transaction rollback and read-only");
  // do stuff
  throw new RuntimeException("readOnlyRollbackExampleTransaction Exception");
 }
 
 public void readWriteCommitExampleTransaction() {
  log.info("-- XmlTransactionTestService.readWriteCommitExampleTransaction -- expects transaction commit");
  // do stuff
 }
 
 public void readWriteRollbackExampleTransaction() {
  log.info("-- XmlTransactionTestService.readWriteRollbackExampleTransaction -- expects transaction rollback");
  // do stuff
  throw new RuntimeException("readWriteRollbackExampleTransaction Exception");
 }
}

When you run this example you will see by the logs that method names starting with readOnly execute in the context of a transaction with read-only semantics and that the other methods execute in the context of a transaction with read-write semantics. You will also notice that the transactions will rollback where an exception is thrown otherwise the transaction will commit.

Declarative - Annotations
The annotation declarative approach configures the transaction attributes via annotations in the Java source file. The sample application for this implementation can be found in the com.mydomain.spring.transaction.declarative.annotations package. 

annotations-test.xml
<?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:tx="http://www.springframework.org/schema/tx"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
 
 <import resource="classpath:transaction-test.xml" />
 
 <bean id="transactionTestService" class="com.mydomain.spring.transaction.declarative.annotations.AnnotationsTransactionTestService" />

   <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

The definition switches on the transactional behaviour, the rest of the configuration is done via annotations in the Java source.

AnnotationsTransactionTestService.java
public class AnnotationsTransactionTestService implements TransactionTestService {
 private final static Log log = LogFactory.getLog(AnnotationsTransactionTestService.class);

 @Transactional(readOnly = true)
 public void readOnlyCommitExampleTransaction() {
  log.info("-- AnnotationsTransactionTestService.readOnlyCommitExampleTransaction -- expects transaction commit and read-only");
  // do stuff
 }
 
 @Transactional(readOnly = true)
 public void readOnlyRollbackExampleTransaction() {
  log.info("-- AnnotationsTransactionTestService.readOnlyRollbackExampleTransaction -- expects transaction rollback and read-only");
  // do stuff
  throw new RuntimeException("readOnlyRollbackExampleTransaction Exception");
 }
 
 @Transactional
 public void readWriteCommitExampleTransaction() {
  log.info("-- AnnotationsTransactionTestService.readWriteCommitExampleTransaction -- expects transaction commit");
  // do stuff
 }
 
 @Transactional
 public void readWriteRollbackExampleTransaction() {
  log.info("-- AnnotationsTransactionTestService.readWriteRollbackExampleTransaction -- expects transaction rollback");
  // do stuff
  throw new RuntimeException("readWriteRollbackExampleTransaction Exception");
 }
}

When you run this example you will see by the logs that the methods that have the readOnly element set to true for the Transactional annotation execute in the context of a transaction with read-only semantics and that the methods that don't execute in the context of a transaction with read-write semantics. You will also notice that the transactions will rollback where an exception is thrown otherwise the transaction will commit.

Programmatic - TransactionTemplate
This is the recommended approach when using programmatic transaction management. The sample application for this implementation can be found in the com.mydomain.spring.transaction.programmatic.template package. 

template-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
 <import resource="classpath:transaction-test.xml" />
 
 <bean id="transactionTestService" class="com.mydomain.spring.transaction.programmatic.template.TemplateTransactionTestService">
  <property name="transactionManager" ref="transactionManager"/>
 </bean>
</beans>

TemplateTransactionTestService.java
public class TemplateTransactionTestService implements TransactionTestService {
 private final static Log log = LogFactory.getLog(TemplateTransactionTestService.class);
 
 private PlatformTransactionManager transactionManager = null;
 
 public PlatformTransactionManager getTransactionManager() {
  return transactionManager;
 }

 public void setTransactionManager(PlatformTransactionManager transactionManager) {
  this.transactionManager = transactionManager;
 }
 
 public void readOnlyCommitExampleTransaction() {
  log.info("-- TemplateTransactionTestService.readOnlyCommitExampleTransaction -- expects transaction commit and read-only");
  final TransactionTemplate tt = new TransactionTemplate(transactionManager);
  tt.setReadOnly(true);
  tt.execute(new TransactionCallbackWithoutResult() {
   public void doInTransactionWithoutResult(TransactionStatus status) {
    log.info("-- doInTransactionWithoutResult -- expects transaction commit");
    // do stuff
   }
  });
 }
 
 public void readOnlyRollbackExampleTransaction() {
  log.info("-- TemplateTransactionTestService.readOnlyRollbackExampleTransaction -- expects transaction rollback and read-only");
  final TransactionTemplate tt = new TransactionTemplate(transactionManager);
  tt.setReadOnly(true);
  tt.execute(new TransactionCallbackWithoutResult() {
   public void doInTransactionWithoutResult(TransactionStatus status) {
    log.info("-- doInTransactionWithoutResult -- expects transaction rollback");
    // do stuff
    status.setRollbackOnly();
   }
  });
 }
 
 public void readWriteCommitExampleTransaction() {
  log.info("-- TemplateTransactionTestService.readWriteCommitExampleTransaction -- expects transaction commit");
  final TransactionTemplate tt = new TransactionTemplate(transactionManager);
  tt.execute(new TransactionCallbackWithoutResult() {
   public void doInTransactionWithoutResult(TransactionStatus status) {
    log.info("-- doInTransactionWithoutResult -- expects transaction commit");
    // do stuff
   }
  });
 }
 
 public void readWriteRollbackExampleTransaction() {
  log.info("-- TemplateTransactionTestService.readWriteRollbackExampleTransaction -- expects transaction rollback");
  final TransactionTemplate tt = new TransactionTemplate(transactionManager);
  tt.execute(new TransactionCallbackWithoutResult() {
   public void doInTransactionWithoutResult(TransactionStatus status) {
    log.info("-- doInTransactionWithoutResult -- expects transaction rollback");
    // do stuff
    status.setRollbackOnly();
   }
  });
 }
}

Programmatic - PlatformTransactionManager
ptm-test.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
 <import resource="classpath:transaction-test.xml" />
 
 <bean id="transactionTestService" class="com.mydomain.spring.transaction.programmatic.ptm.PtmTransactionTestService">
  <property name="transactionManager" ref="transactionManager"/>
 </bean>
</beans>

PtmTransactionTestService.java
public class PtmTransactionTestService implements TransactionTestService {
private final static Log log = LogFactory.getLog(PtmTransactionTestService.class);
 
 private PlatformTransactionManager transactionManager = null;
 
 public PlatformTransactionManager getTransactionManager() {
  return transactionManager;
 }

 public void setTransactionManager(PlatformTransactionManager transactionManager) {
  this.transactionManager = transactionManager;
 }
 
 public void readOnlyCommitExampleTransaction() {
  log.info("-- PtmTransactionTestService.readOnlyCommitExampleTransaction -- expects transaction commit and read-only");
  DefaultTransactionDefinition def = new DefaultTransactionDefinition();
  def.setReadOnly(true);
  TransactionStatus status = transactionManager.getTransaction(def);
  // do stuff
  transactionManager.commit(status);
 }
 
 public void readOnlyRollbackExampleTransaction() {
  log.info("-- PtmTransactionTestService.readOnlyRollbackExampleTransaction -- expects transaction rollback and read-only");
  DefaultTransactionDefinition def = new DefaultTransactionDefinition();
  def.setReadOnly(true);
  TransactionStatus status = transactionManager.getTransaction(def);
  // do stuff
  transactionManager.rollback(status);
 }
 
 public void readWriteCommitExampleTransaction() {
  log.info("-- PtmTransactionTestService.readWriteCommitExampleTransaction -- expects transaction commit");
  DefaultTransactionDefinition def = new DefaultTransactionDefinition();
  TransactionStatus status = transactionManager.getTransaction(def);
  // do stuff
  transactionManager.commit(status);
 }
 
 public void readWriteRollbackExampleTransaction() {
  log.info("-- PtmTransactionTestService.readWriteRollbackExampleTransaction -- expects transaction rollback");
  DefaultTransactionDefinition def = new DefaultTransactionDefinition();
  TransactionStatus status = transactionManager.getTransaction(def);
  // do stuff
  transactionManager.rollback(status);
 }
}

Programmatic or declarative transaction management?
"Programmatic transaction management is usually a good idea only if you have a small number of transactional operations. For example, if you have a web application that require transactions only for certain update operations, you may not want to set up transactional proxies using Spring or any other technology. In this case, using the TransactionTemplate may be a good approach. Being able to set the transaction name explicitly is also something that can only be done using the programmatic approach to transaction management. 

On the other hand, if your application has numerous transactional operations, declarative transaction management is usually worthwhile. It keeps transaction management out of business logic, and is not difficult to configure. When using the Spring Framework, rather than EJB CMT, the configuration cost of declarative transaction management is greatly reduced."