Specify custom isolation levels with the Spring framework

Use custom isolation levels in your Java EE application's distributed transactions

If you're building an application that requires a custom isolation level in a global transaction during the execution of a use case, you've probably found out the hard way that the Java™ Transaction API doesn't support custom isolation levels. Fortunately, the Spring framework lets you design Web and enterprise applications that use custom isolation levels in global transactions. In this article, Ricardo Olivieri walks through two alternative approaches to achieving this goal.

Share:

Ricardo Olivieri (roliv@us.ibm.com), Software Engineer, IBM

Ricardo OlivieriRicardo Olivieri is a software engineer in IBM Integrated Operations. His areas of expertise include design and development of enterprise Java applications for WebSphere Application Server, administration and configuration of WebSphere Application Server, and distributed software architectures. During the last few years, Ricardo has become interested in learning about open source projects such as Drools, Spring, WebWork, Hibernate, and JasperReports. He is a certified Java developer and a certified WebSphere Application Server administrator. He has a B.S. in computer engineering from the University of Puerto Rico Mayaguez Campus.



01 March 2007 (First published 24 October 2006)

Also available in Chinese

Many Java Enterprise Edition (EE) applications access more than one resource when executing a user's request. In such cases, you should use a global transaction to ensure the resources' data integrity. If access to multiple data resources doesn't occur under the same transactional context, one of the resources could be corrupted when an error occurs. Your application can include global transactions by using the Java Transaction API (JTA) transaction manager your application server provides and XA-compliant drivers to connect to the data resources. But your application's requirements might also call (for a variety of technical reasons beyond this article's scope) for a custom isolation level in all data access during the execution of a global transaction -- and JTA transaction managers don't support custom isolation levels.

For example, as part of a use case implementation, the application might query two different database tables and place a message on a message-oriented middleware queue. The design for this use case might require that the two database READ operations be performed using an isolation level of "read committed." It can also be true that during the execution of a different use case, the application should execute those same two database READ operations using a different isolation level, such as "repeatable read." During the execution of the two use cases, the application executes the same database operations and portions of the same code, but it must use different isolation levels. This article explains how you can accomplish this -- thanks to the Spring framework -- even though JTA transaction managers don't support custom isolation levels.

To benefit from the article, you should be familiar with the Spring framework and understand Spring configuration files. Familiarity with your application server, Java EE design patterns, and the concept of global/distributed transactions is also assumed.

A not-so-neat approach

If you need a custom isolation level during the execution of a global transaction and use the Spring framework just to wire your objects together (and don't implement one of this article's solutions), you could end up giving the responsibility for applying the corresponding isolation level to your application's data access objects (DAOs). (As you may recall from experience, DAOs are mainly used to separate business logic from storage-access or persistence code.) For instance, take a look at the following scenario. The data source bean definitions in your application might look like those in Listing 1:

Listing 1. Data source bean definitions
<!-- read committed data source -->
<bean id="dataSourceRC" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName">
    <value>java:comp/env/jdbc/my_datasource_rc</value>
  </property>
</bean>

<!-- repeatable read data source -->
<bean id="dataSourceRR" class="org.springframework.jndi.JndiObjectFactoryBean">
  <property name="jndiName">
    <value>java:comp/env/jdbc/my_datasource_rr</value>
  </property>
</bean>

...

In Listing 1, you can see two data source definitions. One points to a data source object configured to have a "read committed" isolation level, and the other points to a data source configured to have a "repeatable read" isolation level value. One data source definition exists for each isolation level your application uses, so your configuration file could include four data source definitions if your application uses all four isolation levels.

Also, you might use the data source beans in Listing 1 to create instances of the JdbcTemplate class, as shown in Listing 2:

Listing 2. JdbcTemplate bean definitions
<!-- read committed jdbcTemplate -->
<bean id="jdbcTemplateRC" class="org.springframework.jdbc.core.JdbcTemplate" 
  singleton="true">
  
  <property name="dataSource">
    <ref bean="dataSourceRC" />
  </property>
</bean>

<!-- repeatable read jdbcTemplate -->
<bean id="jdbcTemplateRR" class="org.springframework.jdbc.core.JdbcTemplate" 
  singleton="true">
  
  <property name="dataSource">
    <ref bean="dataSourceRR" />
  </property>
</bean>

...

Given this scenario, your DAOs could include logic that determines which JdbcTemplate instance to use depending on the isolation level value for the current running transaction. This value could, for instance, be passed in as a method parameter to your application's DAOs. The caller of the methods in your DAOs would then need to specify the corresponding isolation level depending on the use case being executed.

This approach would work, but including this logic inside the Java code is not the best approach, and maintaining the code would be difficult. In the following sections of this article, I discuss two solutions you can implement to have custom isolation levels in global transactions without needing to turn the objects in the data access layer inside out. Both solutions apply to applications deployed on any application server that uses the data source definition as the location for specifying the isolation level value for database access.


Springing forward

Taking advantage of the Spring framework's transactional features would seem, on the face of it, a better approach. Spring has strong capabilities for defining transactions for an application. The framework lets you specify, in a very clear way, transactional attributes such as isolation levels, propagation behavior, and exception-handling behavior (for example, whether a transaction should be rolled back automatically when particular exceptions are thrown).

The original published version of this article presented a solution based on an older version of the Spring framework. That Spring version always threw an exception if your Spring transaction definition specified a custom isolation level other than ISOLATION_DEFAULT and you used a JTA transaction manager. I showed how to work around this issue by defining custom isolation levels for transactions in a separate proxy rather than in the Spring standard transaction definition. The current Spring framework version lets you state a custom isolation level in your transaction definition when using a JTA transaction manager. But this capability by itself doesn't guarantee that you'll get the specified custom isolation level during the execution of a global JTA transaction. For this to work correctly, you need to perform the additional steps outlined in the following sections.

I present two solutions that allow custom isolation levels in global transactions. The first is generic solution -- not specific to any application server -- and the second is tailored for IBM WebSphere Application Server. The second solution can serve as a template for any other application servers that also offer a proprietary API to manage their resource connection handlers. The first solution requires the latest production-ready version of the Spring framework (2.0.2 at the time of this writing). The second requires the latest Spring nightly snapshot (2.0.3, build 90, at the time of this writing).

You can choose the solution that best fits your needs. Also, using the ideas presented here as a starting point, you may think of alternate solutions that are specifically tailored to your own application's requirements.


Initial Spring configuration for both solutions

Both solutions require four steps. The first step for both is to configure your Spring transaction manager bean. Your transaction manager's allowCustomIsolationLevels flag must be set to true, as shown in bold in Listing 3:

Listing 3. Configuration of a Spring transaction manager bean
<bean id="transactionManager" 
  class="org.springframework.transaction.jta.JtaTransactionManager">
  
  <constructor-arg>
    <ref local="jtaTransactionManager" />
  </constructor-arg>
  <property name="allowCustomIsolationLevels">
    <value>true</value>
  </property>
</bean>

If you don't set the allowCustomIsolationLevels flag to true, its value is false by default. This causes an exception to be thrown if a custom isolation level is specified for a transaction. When you set it to true, you're letting the transaction manager instance know that you'll have a component in your application -- more on this component soon -- that somehow knows how to apply the specified custom isolation levels in your transactions.

Also, note that the transactionManager bean might depend on another bean that implements the javax.transaction.TransactionManager interface. The need to pass in an instance of the javax.transaction.TransactionManager interface as a constructor argument depends on the application server you're using. In many cases, a plain JtaTransactionManager definition is all you need. In the bean definition in Listing 3, a reference to a bean identified as jtaTransactionManager that implements the TransactionManager interface is passed in as a constructor argument. The definition of the jtaTransactionManager bean also depends on the application server you are working with. For instance, if you are using IBM WebSphere Application Server, the definition of the jtaTransactionManager bean should look like Listing 4:

Listing 4. JTA transaction manager definition for IBM WebSphere Application Server
<bean id="jtaTransactionManager" 
  class="org.springframework.transaction.jta.WebSphereTransactionManagerFactoryBean" 
  singleton="true" />

Generic solution

For an application-server-independent way to specify custom isolation levels in global transactions, you can use an instance of the org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter class as the component that knows how to apply the isolation levels. It's quite simple to create and configure an instance of this class. One nice thing about it is that it implements the javax.sql.DataSource interface. This means that you can easily modify your current Spring configuration file so that all beans that include a reference to your data source objects now use an instance of this class instead; you needn't write or change a single line of Java code for this purpose. You'll see soon why you would want to take advantage of this capability.

The second step in this solution is to add this component in your global transactions. To do this, you can route all the method calls that would otherwise be sent to your data source objects to an instance of the IsolationLevelDataSourceRouter class. During the execution of a global transaction, this instance then routes the method calls to the corresponding data source object (such as dataSourceRC or dataSourceRR in Listing 1) based on the isolation level specified for the current running transaction. So you need to modify your configuration file so that all references to your original data source objects now point to an instance of the IsolationLevelDataSourceRouter class. (You can think of this as a before aspect-oriented advice applied to your application's data source beans.) Listing 5 shows the bean definition for the IsolationLevelDataSourceRouter instance:

Listing 5. IsolationLevelDataSourceRouter bean definition
<bean id="dataSourceRouter" 
  class="org.springframework.jdbc.datasource.lookup.IsolationLevelDataSourceRouter" 
  singleton="true">
  
  <property name="targetDataSources">
    <map>
      <entry key="ISOLATION_REPEATABLE_READ" 
        value="java:comp/env/jdbc/my_datasource_rr />
      <entry key="ISOLATION_SERIALIZABLE" 
        value="java:comp/env/jdbc/my_datasource_ser" />
      <entry key="ISOLATION_READ_UNCOMMITTED" 
        value="java:comp/env/jdbc/my_datasource_ru" />
      <entry key="ISOLATION_READ_COMMITTED" 
        value="java:comp/env/jdbc/my_datasource_rc" />
    </map>
  </property>
  <property name="defaultTargetDataSource" value="java:comp/env/jdbc/my_ds_rc" />
</bean>

The IsolationLevelDataSourceRouter instance uses the mappings (that is, isolation level value to data source object association) specified in its definition to determine how to switch transparently to the appropriate data source object based on the current transaction's isolation level. (Remember that a current transaction's isolation level is specified in the properties of a Spring transaction definition.) A direct benefit of using the IsolationLevelDataSourceRouter class is that now you need only one JdbcTemplate instance that uses the IsolationLevelDataSourceRouter instance as its dataSource property.

The third step, then, is to update your JdbcTemplate bean definition to reference the IsolationLevelDataSourceRouter bean defined in the preceding step, as shown in Listing 6:

Listing 6. Single JdbcTemplate definition referencing the IsolationLevelDataSourceRouter bean
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" 
  singleton="true">
  
  <property name="dataSource">
    <ref bean="dataSourceRouter" />
  </property>
</bean>

The fourth and final step is to define your transactional proxy. The bean definition for this proxy should state the transactional attributes for each method of your service object (see What are service objects?) and the transaction manager that should be used (which must be a reference to the bean defined in Listing 3). For instance, Listing 7 shows the definition of a transactional proxy for a service object named OrderService:

Listing 7. Transactional proxy definition using a JTA transaction manager
<bean id="orderService" 
  class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    
  <property name="transactionManager">
    <ref local="transactionManager" />
  </property>
  <property name="proxyInterfaces">
    <list>
      <value>sample.services.OrderService</value>
    </list>
  </property>
  <property name="target">
    <ref local="orderServiceTarget" />
  </property>
  <property name="transactionAttributes">
    <props>
      <prop key="save*">
        PROPAGATION_REQUIRED, ISOLATION_READ_COMMITTED</prop>
      <prop key="delete*">
        PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED</prop>
      <prop key="find*">
        PROPAGATION_REQUIRED, ISOLATION_REPEATABLE_READ,readOnly</prop>
    </props>
  </property>
</bean>

What are service objects?

In this article's context, you can think of service objects as facades responsible for hiding business components and centralizing workflow. Their methods define the implementation for an application's usage scenarios. A service object provides coarse-grained interfaces to clients (Web UI, remote services client, and so on). The Java EE Session Facade design pattern (see Resources) lends itself perfectly to service objects. (In the EJB world, session facades are implemented by enterprise session beans.)

Using the IsolationLevelDataSourceRouter instance and this transactional proxy bean results in the following scenario when the orderService bean's save() method is invoked:

  1. The transaction manager starts a transaction.
  2. A custom isolation level of ISOLATION_READ_COMMITTED is set as the current transaction's isolation level value.
  3. All method calls for retrieving database connections are routed to the IsolationLevelDataSourceRouter instance.
  4. Whenever the IsolationLevelDataSourceRouter bean receives a method call, it determines which data source it should route the call to by examining the current isolation level (ISOLATION_READ_COMMITTED in this case) specified in the Spring transaction definition.
  5. The IsolationLevelDataSourceRouter instance delegates the method calls it receives to the corresponding data source. In this case it's the data source identified by the Java Naming and Directory Interface (JNDI) name java:comp/env/jdbc/my_datasource_rc, because this is the data source that was bound to the "read committed" isolation level. (See the IsolationLevelDataSourceRouter bean definition in Listing 5.) This guarantees that the connection objects used during the execution of this transaction have an isolation level of "read committed."

The sequence diagram in Figure 1 illustrates the scenario I've just described. There you can see the flow of execution when a client object invokes the OrderService instance's save() and find() methods. The IsolationLevelDataSourceRouter instance handles the task of determining which data source should be used to get a resource connection. All DAOs can reference a single JdbcTemplate instance, while the IsolationLevelDataSourceRouter instance is responsible for obtaining a connection object from the corresponding data source object.

Figure 1. Sequence diagram for the save() and find() methods with use of IsolationLevelDataSourceRouter
Sequence diagram for save() and find() methods - IsolationLevelDataSourceRouter

See the full figure here.

Note that a benefit of using the IsolationLevelDataSourceRouter class is that you can use the data sources' JNDI names to reference them. You don't need separate bean definitions that point to the data source objects that exist in your application server's JNDI namespace. (Of course, if you'd like to, you can still have them.) As the Spring documentation says, "this allows for a single concise definition without the need for separate DataSource bean definitions."

Data sources in deployment descriptors

Application servers have their own proprietary way of defining data sources and their isolation levels. Commonly, they provide deployment descriptor extensions to associate a data source definition with a specified isolation level value.

To summarize, you can use the IsolationLevelDataSourceRouter class to route to a corresponding underlying data source object based on the isolation level specified in the Spring transaction definition. This lets you have custom isolation levels in your global JTA transactions. Using a IsolationLevelDataSourceRouter instance also lets you reference your data source objects without including separate bean definitions for them in your configuration file. However, note that even though you might not need multiple bean definitions for your data source objects in the Spring configuration file, at the Web deployment descriptor level, you still must define a data source for each isolation level you use (see Data sources in deployment descriptors).


Proprietary solution

The proprietary solution requires a little less setup work: you don't need to define a data source object in your Web deployment descriptor for each isolation level you intend to use in your application; you need just one. The downside of this approach is that it requires the use of a proprietary API that's specific to your application server. I prefer this option because the reduced setup requirements can benefit environments where no development team members have administrative access rights. IBM WebSphere Application Server is the commercial application server that I'm most familiar with, so I used that in this solution.

As in the generic solution, you need a component that receives all the method calls that otherwise would be sent to the original data source instances. It's then up to this component to apply the custom isolation level. In this case, the component that knows how to apply the custom isolation levels is an instance of the org.springframework.jdbc.datasource.DelegatingDataSource class. This class also implements the javax.sql.DataSource interface, which makes it extremely easy to substitute all references to your original data source bean definitions with an instance of this class.

A class named WebSphereDataSourceAdapter (see Resources), which inherits methods from the org.springframework.jdbc.datasource.DelegatingDataSource and org.springframework.jdbc.datasource.IsolationLevelDataSourceAdapter classes, was originally written by Lari Hotari and this author and then further modified by Juergen Hoeller. (The original class was submitted as a feature to the Spring framework and, at the time of this writing, it was available in the latest 2.0.3 nightly snapshot; see Resources.) An instance of this class can serve as the component that knows how to apply custom isolation levels for transactions running in WebSphere Application Server.

All method calls that would be sent to your data source objects must now be routed to an instance of the WebSphereDataSourceAdapter class. During the execution of a transaction, this WebSphere data source adapter relies on the IsolationLevelDataSourceAdapter class's getCurrentIsolationLevel() method to determine the isolation level value that should be used for the current running global transaction. (This value, just as in the generic solution, is determined by the properties of the Spring transaction definition.) The implementation of the getCurrentIsolationLevel() method invokes org.springframework.transaction.support.TransactionSynchronizationManager class's static getCurrentTransactionIsolationLevel() method to get this value.

Once the isolation level value is retrieved, it's used to obtain a connection object with the required isolation level value from the WebSphere Application Server data source class implementation (com.ibm.websphere.rsadapter.WSDataSource). The WSDataSource class requires callers of its methods to use an instance of the com.ibm.websphere.rsadapter.JDBCConnectionSpec class (see Resources) to state the transactional properties of the connection that should be returned. An instance of the JDBCConnectionSpec class can be used to specify which isolation level is needed on the connection returned from the WSDataSource instance. The JDBCConnectionSpec class's setTransactionIsolation() method is used to specify the isolation level that is needed for the current transaction. The implementation of WebSphereDataSourceAdapter is quite simple; it just serves as a "smart" proxy that knows how to obtain a connection handler with the right isolation level.

The proprietary API from IBM lets you write application-server-specific code that asks for database connections with a custom isolation level. The main benefit of this approach is that you don't need to define a data source (at the deployment descriptor level) for each isolation level you use in your application's transactions. Instead, you define only one data source and then ask that data source for a connection with the corresponding isolation level.

This solution's second step, then, is to add a bean definition to your Spring configuration file for the WebSphereDataSourceAdapter class, as shown in Listing 8:

Listing 8. Definition of the WebSphereDataSourceAdapter bean
<bean id="dataSourceAdapter" 
  class="org.springframework.jdbc.datasource.WebSphereDataSourceAdapter">
  
  <property name="targetDataSource">
    <bean class="org.springframework.jndi.JndiObjectFactoryBean">
      <property name="jndiName" value="java:comp/env/jdbc/my_datasource" />
    </bean>
  </property>
  <property name="isolationLevel">
    <bean class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
      <property name="staticField">
        <value>java.sql.Connection.TRANSACTION_READ_COMMITTED</value>
      </property>
    </bean>
  </property>

  <!-- 
  Note: Username and password are used only when the application resource
  reference is using res-auth = Application. If the username is "empty",
  then this class will simply delegate to the standard getConnection()
  method.
  -->
  <property name="username">
    <value>username</value>
  </property>
  <property name="password">
    <value>password</value>
  </property>
</bean>

WebSphereDataSourceAdapter's targetDataSource property (which is inherited from the DelegatingDataSource class) must be an instance of the proprietary WSDataSource class. (Otherwise, an exception is thrown.) As Listing 8 shows, this is the data source object that exists for your application in the WebSphere Application Server JNDI namespace. The isolationLevel property (which is inherited from the IsolationLevelDataSourceAdapter class) states what the isolation level value should be if no transaction is currently running, and consequently there's no isolation level value to use. For the bean definition in Listing 8, this default isolation level value is TRANSACTION_READ_COMMITTED. The WebSphereDataSourceAdapter class also lets application developers specify a username and password for cases when the application, not the container, handles authentication. You use the username and password properties to specify these credentials.

The third step is to update your JdbcTemplate bean definition as shown in Listing 9. This bean definition should reference the WebSphereDataSourceAdapter bean that was defined in the preceding step. This guarantees that the WebSphereDataSourceAdapter instance "intercepts" all calls before they are dispatched to the WebSphere Application Server data source object.

Listing 9. Single JdbcTemplate definition referencing the WebSphereDataSourceAdapter bean
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" 
  singleton="true">
  
  <property name="dataSource">
    <ref bean="dataSourceAdapter" />
  </property>
</bean>

The last step is the same as the generic solution's: define a transactional proxy bean. So a bean definition like the one shown in Listing 7 is also needed for this solution.

The sequence diagram shown in Figure 2 illustrates the flow of execution for the OrderService instance's save() and find() methods when an instance of the WebSphereDataSourceAdapter class is used as the component that applies the corresponding isolation level value for transactions:

Figure 2. Sequence diagram for the save() and find() methods with use of WebSphereDataSourceAdapter
Sequence diagram for save() and find() methods - WebSphereDataSourceAdapter

See the full figure here.

To summarize, this second solution requires the implementation of a delegating data source that knows how to obtain a connection from the underlying application server's data source with the corresponding isolation level. This option requires the use of an API that's specific to the application server you're using. You need to define only one data source at the deployment descriptor level. Then, when retrieving connections from that data source, you use your application server's proprietary API to specify the isolation level value for that connection.

A similar solution to the one I've presented here can be developed for other application servers that offer a proprietary API to manage their resource connections and use the data source definition as the location for specifying the isolation level value for database access. You would just need to extend the IsolationLevelDataSourceAdapter class and write the server-specific code there. You can easily use the WebSphere solution I've presented as a template for building solutions specific to other application servers.


Conclusion

Using the Spring framework, your application can easily use a custom isolation level during the execution of a distributed transaction. The two solutions shown in this article achieve this goal using Spring's dependency-injection capabilities and Spring's standard transaction definition. Both eliminate the need to specify the isolation level for a distributed transaction in a separate proxy or component. You also achieve total decoupling of the classes that implement an application's business and persistence logic from the components that ensure that the same isolation level is used during the execution of the global transaction.

Acknowledgments

The author would like to thank Coraly Romero-Principe for her help in making this article possible and Lari Hotari for starting the effort and design for the WebSphereDataSourceAdapter class.


Download

DescriptionNameSize
Source codej-isolation-WebSphereDataSourceAdapter.java12KB

Resources

Learn

Get products and technologies

  • Download IBM product evaluation versions and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, WebSphere
ArticleID=169734
ArticleTitle= Specify custom isolation levels with the Spring framework
publish-date=03012007