Enterprise Java

Spring Transactions Visibility

Spring, on initializing application context, creates proxies when it comes across classes marked with @Transactional. @Transactional can be applied at class level or method level. Applying it at class level means all the public methods defined in the class are transactional. The kind of proxy Spring creates i.e. Jdk Proxy or CGLIB proxy, depends on the class in which the method is marked transactional. If the class implements atleast one interface, Spring creates a Jdk dynamic proxy. This proxy implements the same interface as the original class does and intercepts the interface methods with transaction maintenance logic. It delegates the call to the original object composed in it. Suppose the class does not implement any interface, Spring creates a CGLIB proxy. This proxy extends the original class and overrides the public methods. We will take a closer look at this soon. Suppose we have a class defined like this:

public interface BookDao{
	void buyBook(String isbn) throws BookNotFoundException;
	Book findByIsbn(String isbn);
	int deductStock(Book book);
}

public class JdbcBookDao implements BookDao{
	void buyBook(String isbn) throws BookNotFoundException{
		Book book = findByIsbn(isbn);
		if(book == null){
		throw new BookNotFoundException();
		}
		deductStock(book);
	}

	@Transactional(propagation=Propagation.REQUIRES_NEW)
	Book findByIsbn(String isbn){
		Book book = getJdbcTemplate().queryForObject("SELECT * FROM BOOK WHERE ISBN=?",
		ParameterizedBeanPropertyRowMapper.newInstance(Book.class), isbn);
		return book;
	}

	@Transactional(propagation=Propagation.REQUIRES_NEW)
		int deductStock(Book book){
		String sql = "UPDATE BOOK_STOCK SET STOCK=STOCK-1 WHERE BOOK_ID=?";
		return getJdbcTemplate().update(sql, stockIncrement, book.getId());
	}
}

Now will Spring automatically create a transaction on calling bookDao’s findByIsbn from main method? No. We have to declare this in the xml configuration:

<tx:annotation-driven>

So if it does not create a transaction, will it throw an error? Answer is again a No. Spring executes this statement non-transactionally.

Once we have declared ‹tx:annotation-driven›, as rightly expected, Spring creates a Jdk dynamic proxy for JdbcBookDao as the class implements an interface. Now say, we call JdbcBookDao’s buyBook method, how many transactions are created by Spring?

  1. Two: 1 for findByIsbn and 1 more for deductStock.
  2. One: Both findByIsbn and deductStock in same tx.
  3. No transaction at all!

The answer is (3). The default transaction mode is ‘proxy’ for transactions. What this means is method calls made through proxy only will be considered for automatic transaction management by Spring. Now if you carefully observe, buyBook method is not marked transactional. And hence when transactional proxy is created, this method does not get intercepted with transaction management logic as it is not marked @Transactional. In short, buyBook is not overridden in proxy. And hence the method directly gets invoked on the original object. And so the other two methods also gets called on the original object. Remember that only PROXY contains transaction management code. So, as the other methods too get called on original object, Spring does not create a transaction at all. Now will the problem be solved if we mark buyBook as @Transactional? Will Spring creates two separate transactions for each findByIsbn and deductStock methods?

No. Spring creates only one transaction when buyBook() gets called. It would not create any new transaction further as the respective methods gets called on the original object itself and not the proxy. So how to solve this problem?

Can we ask Spring to create a CGLIB proxy()? Now as the proxy is a subclass with overridden public transactional methods, it creates a new transaction for each of the method call? Again NO. The CGLIB proxy does not directly call the method on super class. Here is a rough idea of how it is implemented.

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class MyInterceptor implements MethodInterceptor {
	private Object realObject;

	public MyInterceptor(Object original) {
		this.realObject = original;
	}

	// This method will be called every time the object proxy calls any of its methods
	public Object intercept(Object o, Method method, Object[] args,
                          MethodProxy methodProxy) throws Throwable {
		/**
		* Transaction Management Code
		*/

		// Invoke the method on the real object with the given params
		Object res = method.invoke(realObject, args);

		/**
		* Transaction Management Code
		*/
		return res;
	}
}

import net.sf.cglib.proxy.Enhancer;

public class ProxyCreator(){

	public static T createProxy(T original){
		// Enhancer is CGLIB class which builds a dynamic proxy with new capabilities
		Enhancer e = new Enhancer();
		e.setSuperclass(original.getClass());

		// We have to declare the interceptor whose 'intercept' will be called when methods are called on proxy.
		e.setCallback(new MyInterceptor(original));
		T proxy = (T) e.create();
		return proxy;
	}
}

So as you have seen here, the proxy extends the original class and is composed with it’s object. So when we call buyBook, the proxy creates a transaction and delegates the call to original object. No as findByIsbn and deductStock gets called from buyBook of original object, no new transactions get created.

A quick turnaround solution for this would be as JdbcBookDao is a singleton, get this object from the application context. Now instead of calling methods directly on the object, call it using the reference(to make sure the proxy gets called) Here is how the method may now look like.

public class JdbcBookDao implements BookDao, ApplicationContextAware{
	private ApplicationContext context;
	private BookDao bookDao;

	public void setApplicationContext(ApplicationContext context){
		this.context = context;
	}

	public BookDao getBookDao(){
		bookDao = (BookDao)context.getBean("jdbcBookDao");
	}

	void buyBook(String isbn) throws BookNotFoundException{
		Book book = getBookDao().findByIsbn(isbn);
		if(book == null){
			throw new BookNotFoundException();
		}
		getBookDao().deductStock(book);
	}

	.....
}

Just implemented a crude version to get this up and working. We can definitely improve on the way it is designed. Instead of directly injecting application context into the DAO, may be we can have a kind of helper class which does that. Or another alternative for getting this done is to use programmatic transactions.

A final point to note is that Spring manages transactions only when public methods are marked transactional. For private, protected and package-private methods, Spring does not provide transaction management support. In case of Dynamic proxies, as they implement an interface, all transactional methods are public. So no need to worry about non-public methods. And in case of CGLIB proxies, only public methods get overridden when subclass is created. So even here non-public methods are not considered.

Let me end this discussion with a question. When I tried to proxy target class using ‹tx:annotation-driven proxy-target-class=”true”/›, it really did not work i.e. CGLIB proxies are not created. For this to work, I have to do a minor hack. As spring documentation says clearly if proxy-target-class is enabled on any of ‹tx:annotation-driven›, ‹aop:config› or ‹aop:aspectj-autoproxy›, Spring will enable CGLIB proxy creation on the container. So I just created an empty ‹aop:config proxy-target-class=”true”/›. And not to worry, it started working! Not sure if it’s a bug in Spring itself. Highly appreciated if some one can answer this.
 

Reference: Spring Transactions Visibility from our JCG partner Prasanth Gullapalli at the prasanthnath blog.

Prasanth Gullapalli

Prasanth is passionated about technology and specializes in application development in distributed environments. He has always been fascinated by the new technologies and emerging trends in software development.
Subscribe
Notify of
guest

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

0 Comments
Inline Feedbacks
View all comments
Back to top button