Automatic transaction management with POJOs

Currently I'm working on project that started anew some months ago. Even if it's not a legacy project, the client (a public administration company) imposed a list of technologies that are considered "stable". This means: jdk 1.3.1, struts 1.2 + jsp, stateless session beans on weblogic 7, and pure jdbc access to the database. After some futile resistance trying to introduce some "new" libraries and tools, we're now here kneading this stuff... but, that's not that bad: real problems comes with huge "freaky guidelines", but maybe I'll speak about this another time.

So we started playing plain JDBC within stateless session beans. Early we realized that packaging an EAR and deploying it on a remote server, everytime it's a painful and time wasting task. Then, we started replacing EJB transaction management with our-own dynamic proxies that simulates the connection and transaction handling (tx-required attribute on ejb methods) on local POJOs.
We've written a Locator object that, given an interface retrieves the business component that implements it, and does the job with POJOs just as it works with EJBs.

The client code then is something like:

    Service service = (Service)Locator.locate(Service.class);
    service.doSomething();

The Locator is aware of the fact that it is running on development environment (tomcat + pojos + plain jdbc connection) or production environment (weblogic + ejbs + datasource).
To indicate to work with pojos or with ejbs we just used a system property:

public class Locator {
    private static HandlerFactory handlerFactory;
    private static IDataSource dataSource;

    static {
        if (Boolean.getBoolean("myApp.local")) {
            handlerFactory = new LocalHandlerFactory();
            dataSource = new LocalDataSource();
        } else {
            handlerFactory = new RemoteHandlerFactory();
            dataSource = new RemoteDataSource();
        }
    }

    public static Object locate(Class serviceInterface) {
        if (serviceInterface == IDataSource.class)
            return dataSource;
        return Proxy.newProxyInstance(serviceInterface.getClassLoader(),
                new Class[] { serviceInterface }, handlerFactory
                        .getHandler(serviceInterface));
    }
}

If the System property myApp.local is set to true, the locator object will use a LocalHandlerFactory that returns POJOs instead of stateless session beans, and a plain JDBC connection instead of a DataSource, and the proxy for the pojo will handle the transaction as for EJBs. In opposite, the RemoteHandlerFactory (not listed here) will retrieve the EJB from jndi and invoke the method on it.

The LocalDelegateHandler will use the LocalDataSource to handle transactions and database connections.

public class LocalDelegateHandler implements InvocationHandler, Serializable {
    // ...

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        logger.debug(method.getName() + " has been called");
        long before = System.currentTimeMillis();
        try {
            Object result = null;
            try {
                LocalDataSource.incNestingLevel();
                Method targetMethod = target.getClass().getMethod(
                        method.getName(), method.getParameterTypes());
                result = targetMethod.invoke(target, args));
            } finally {
                LocalDataSource.decNestingLevel();
            }
            LocalDataSource.handleCommit();
            return result;
        } catch (InvocationTargetException e) {
            logger.debug("invocationtargetException: " + e.getMessage());
            Throwable target = e.getTargetException();
            if (target instanceof RuntimeException) {
                LocalDataSource.handleRollback();
            } else {
                LocalDataSource.handleCommit();
            }
            throw target;
        } finally {
            logger.debug(method.getName() + " has finished, execTime = "
                    + (System.currentTimeMillis() - before) + "ms.");
        }
    }
}

The incNestingLevel and decNestingLevel are used to handle reentrancy. The handleCommit and handleRollback, notify that the transaction should be committed or rolled back, before returning the method call.

The LocalDataSource will implement connection handling (open/close) and transaction management (commit/rollback).
It will open only a connection for method invokation on the service proxy, and will rollback or commit at the end of the call on the proxy instance, also handling reentrancy. It seems to work quite good for our development process (in production it will delegate this job to weblogic).

public class LocalDataSource implements IDataSource {
    private static Logger logger = Logger.getLogger(LocalDataSource.class);
    private static final String DRIVER = Configuration.getProperty("jdbc.driver");
    private static final String URL = Configuration.getProperty("jdbc.url");
    private static final String USER = Configuration.getProperty("jdbc.user");
    private static final String PASS = Configuration.getProperty("jdbc.pass");
    
    private static ThreadLocal context = new ThreadLocal() {
        protected Object initialValue() {
            return new Context();
        }
    };
    
    public static Context getContext() {
        return (Context)context.get();
    }
    
    public static class Context {
        private Connection connection;
        private int nestingLevel;
        
        public Connection getConnection() {
            return connection;
        }
        public void setConnection(Connection connection) {            
            this.connection = connection;            
        }
        public int getNestingLevel() {
            return nestingLevel;
        }
        public void setNestingLevel(int nestingLevel) {
            this.nestingLevel = nestingLevel;
        }
        public void incNestingLevel() {
            nestingLevel++;
        }
        public void decNestingLevel() {
            nestingLevel--;
        }
    }
    
    public static void handleCommit() {    	
        LocalDataSource.Context ctx = LocalDataSource.getContext(); 
        if (ctx.getNestingLevel() == 0) {
            Connection conn = ctx.getConnection();
            if (conn != null) {
                try {
                    logger.debug("committing transaction");
                    conn.commit();
                } catch (SQLException e) {
                    throw new EJBException(e);
                } finally {
                    closeConnection();
                }
            }
        }
    }

    public static void handleRollback() {    	
        LocalDataSource.Context ctx = LocalDataSource.getContext(); 
        Connection conn = ctx.getConnection();
        if (conn != null) {
            try {
                logger.debug("rolling back transaction");
                conn.rollback();
            } catch (SQLException e) {
                throw new EJBException(e);
            } finally {
                closeConnection();
            }
        }
    }

    private static void closeConnection() {
        Connection conn = getContext().getConnection();
        if (conn != null) {
            try {                
                conn.close();
                getContext().setConnection(null);
                logger.debug("connection has been closed");
            } catch (SQLException e) {
            }
        }
    }

    public Connection open() throws SQLException {
        try {
            if (getContext().getConnection() == null) {                
                Class.forName(DRIVER);
                Connection conn = DriverManager.getConnection(URL, USER, PASS);
                conn.setAutoCommit(false);
                getContext().setConnection(conn);
                logger.debug("a new connection has been opened");
            }           
            return getContext().getConnection();           
        } catch (ClassNotFoundException e) {
            throw new EJBException("Driver not found: " + e.getMessage(), e);
        }
    }

    public static void incNestingLevel() {
        LocalDataSource.getContext().incNestingLevel();        
    }

    public static void decNestingLevel() {
        LocalDataSource.getContext().decNestingLevel();        
    }

    public void close(Connection connection) throws SQLException {
    }
}

In conclusion, when I call from a client :

    Service service = (Service)Locator.locate(Service.class);
    service.doSomething();

    ... supposing that, inside Service.doSomething I have ...

    public void Service.doSomething() {
        try {
            IDataSource dataSource = (IDataSource) Locator.locate(IDataSource.class);
            Connection conn = null;
            try {
                conn = dataSource.open();
                // do something
                AnotherService another = (AnotherService)Locator.locate(AnotherService.class);
                another.doSomethingElse();
            } finally {
                dataSource.close(conn);
            }
        } catch (SQLException sqlEx) {
            throw new EJBException(sqlEx);
        }
    }

    ... and inside AnotherService.doSomethingElse() ...

    public void AnotherService.doSomethingElse() {
        try {
            IDataSource dataSource = (IDataSource) Locator.locate(IDataSource.class);
            Connection conn = null;
            try {
                conn = dataSource.open();
                // do something else
            } finally {
                dataSource.close(conn);
            }
        } catch (SQLException sqlEx) {
            throw new EJBException(sqlEx);
        }
    }

I will always have a single connection that will be passed transparently; then:

  • if a runtime exception will occur, the transaction will be rolledback instantly and the method call will return;
  • if a checked exception will occur, the transaction will be committed and the method will return to the client with the raised exception;
  • otherwise, if everything goes well, the transaction will be committed and the method will return to the client normally.

In any case the connection will properly be closed.

Here is the output of a TestCase:

2006-10-19 18:54:28,405 [DEBUG][ServiceBean] doSomething has been called
Connection.setAutoCommit(false);
2006-10-19 18:54:28,421 [DEBUG][LocalDataSource] a new connection has been opened
2006-10-19 18:54:28,421 [DEBUG][AnotherServiceBean] doSomethingElse has been called
2006-10-19 18:54:28,421 [DEBUG][AnotherServiceBean] doSomethingElse has finished, execTime = 0ms.
2006-10-19 18:54:28,421 [DEBUG][LocalDataSource] committing transaction
Connection.commit();
Connection.close();
2006-10-19 18:54:28,421 [DEBUG][LocalDataSource] connection has been closed
2006-10-19 18:54:28,421 [DEBUG][ServiceBean] doSomething has finished, execTime = 16ms.

As you can see (in red), even if two connections have been requested (one by Service.doSomething(), and another one by AnotherService.doSomethingElse()), just only a connection has been created, and the commit has been called just before the close() and returning to the client. As I wanted it to behave: two methods in a single transaction, sharing the same connection transparently.
In green, are displayed messages printed by the mock sql Connection object, that I used to test this behavior.
This is the same behavior you have using EJBs with transaction attribute set to "Required" on interface methods; that is a very common configuration for transaction management on EJBs.

And here you find the whole source code: ProxyForTXPojo.zip


One Response to “Automatic transaction management with POJOs”  

  1. 1 Trimming JDBC - NewInstance


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>



Calendar

October 2006
M T W T F S S
« Sep   Nov »
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Follow me

twitter flickr LinkedIn feed

Subscribe by email

Enter your email address:

Archives


Categories

Tag Cloud


Listening