The first time the architect required to apply a pattern for opening and closing database sessions and transactions it was fine. The project had already lots of business methods using different algorithms, so some order was urgently needed. It took a few days of a senior developer to apply the pattern. But afterwards the architect decided that nested calls between business methods was not supported, so the pattern was enhanced. Fortunately we didn't apply these changes, because in a third iteration the architect decided that XA transactions were not supported and the pattern should be changed again.
The problem
We have a large collection of business components and methods whose transactional behavior may change depending on non functional requirements. The pattern always follows the same sequence of events : open session, begin transaction, perform business operations, commit transaction and close session. We want to extract the common code out of the business methods and leave only pure business logic.
Proposed solution
In read an article in TheServerSide about how Spring framework uses the library CGLIB to declaratively perform transaction management. CGLIB allows to dynamically create subclasses of any object at runtime and to intercept its methods by providing callback classes. The proof of concept described here explains how to do it.
Class diagram
Classes in blue belong to CGLIB framework. The yellow ones are the ones created for the sample.
The business methods are encapsulated in PersonBC class :
- getPerson obtains a Person pojo by identifier and is not transactional.
- savePerson stores a Person pojo in the database. The Transactional annotation is used later by the TxManager to perform transaction boilerplate actions.
Listing 1 : PersonBC.java
package bc; import java.util.Random; import pojo.Person; import common.Transactional; import exception.DatabaseException; public class PersonBC { public Person getPerson(Integer personId){ System.out.println("select * from person where..."); return new Person(1,"Yannick"); } @Transactional public void savePerson(Person person) throws DatabaseException{ System.out.println("update Person set..."); /* Random exception */ if(new Random().nextInt()%2 == 0){ throw new DatabaseException(); } } } |
Instances of PersonBC and other business objects are created by BusinessFactory. CGLIB Enhancer is used to create proxies for the business objects.
Listing 2 : BusinessFactory.java
package common; import net.sf.cglib.proxy.Enhancer; public class BusinessFactory { static TxManager callback = new TxManager(); /* Create enhanced BC */ public static Object newInstance( Class clazz ){ Enhancer e = new Enhancer(); e.setSuperclass(clazz); e.setCallback(callback); return e.create(); } } |
TxManager is the class responsible to intercept method calls from the business proxies. It checks for the presence of the Transactional annotation to determine if transaction actions must be performed. Session and transaction management methods are empty as this is a proof of concept. In the real world a ORM framework like Hibernate may be used.
Listing 2 : TxManager.java
package common; import java.lang.reflect.Method; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class TxManager implements MethodInterceptor { /* Interceptor method */ public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { if(method.isAnnotationPresent(Transactional.class)){ return doTransactional(obj,method,args,proxy); }else{ return doNonTransactional(obj,method,args,proxy); } } /* Performs method by adding session and transaction management */ private Object doTransactional(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable{ boolean isOK = false; openSession(); beginTransaction(); try { Object superReturn = proxy.invokeSuper(obj, args); commit(); isOK = true; return superReturn; } finally { if(!isOK){ rollBack(); } closeSession(); } } /* Performs method by adding session management */ private Object doNonTransactional(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable{ openSession(); try { Object superReturn = proxy.invokeSuper(obj, args); return superReturn; } finally { closeSession(); } } /* Fake open session */ private void openSession(){ System.out.println("Open session"); } /* Fake begin transaction */ private void beginTransaction(){ System.out.println("Begin transaction"); } /* Fake commit */ private void commit(){ System.out.println("Commit transaction"); } /* Fake rollback */ private void rollBack(){ System.out.println("Rollback transaction"); } /* Fake close session */ private void closeSession(){ System.out.println("Close session"); } } |
The PersonController class represents the client that will usually reside in a Web container.
Listing 3 : PersonController.java
package action; import common.BusinessFactory; import exception.DatabaseException; import pojo.Person; import bc.PersonBC; public class PersonController { public static void main(String[] args) throws Exception { PersonBC bc = (PersonBC)BusinessFactory.newInstance(PersonBC.class); Person person = bc.getPerson(1); System.out.println("----------------"); try { bc.savePerson(person); System.out.println("Update ok"); } catch (DatabaseException e) { System.out.println("Cannot update person"); } } } |