Thursday, April 26, 2007

Long-running Processes with Spring DAO and Hibernate

Spring and Hibernate are increasingly used together in Java web applications. Spring is used as the MVC and dependency-injection framework and also provides support for data access, transaction management etc. Hibernate is usually used alongside Spring as the object-relational mapping framework. Spring's support for Hibernate is impressive through it's DAO templating mechanism. This truly simplifies matters as all the routine infrastructure plumbing code is taken care of by Spring.

Spring intercepts requests made to the webapp and through dependency-injection makes objects available in order to fulfil the request. This includes creating the Hibernate Session object used by the DAOs. The Session object is lightweight and created and destroyed for every request. Hibernate sessions are not threadsafe and should be used by only one thread at a time. In order to keep the session open throughout the lifetime of a request we tie it to the view. This is done either by using Spring's OpenSessionInViewInterceptor or OpenSessionInViewFilter as below:

<bean id="openSessionInViewInterceptor" 
    class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
    <property name="sessionFactory" ref="sessionFactory" />
    <property name="flushModeName" value="Flush_AUTO" />
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    ...
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    ...
</bean>

Most requests are synchronous and completed before the response is returned to the client. However, in the case of long-running processes, a new thread is created for execution of the process. The response is returned back to the client and the process runs independently on the server. Hibernate sessions obtained through OpenSessionInViewInterceptor are no longer available to the process as they are closed once the view has returned. This throws a LazyInitializationException complaining that the owning session was closed.

The problem can be overcome by directly accessing the Hibernate SessionFactory bean and binding the session to the long-running process thread. Hence the session is available and open within the thread itself. The UML diagram below describes our objects.

TestController is the Controller class that handles the request. Since it implements the ServletContextAware interface, Spring automatically passes the ServletContext object to it. This is used to obtain the ApplicationContext object. Next we create an instance of our LongProcessInvoker class, pass ApplicationContext to it and finally start execution using an Executor.

// TestController.java
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
    try {
        ApplicationContext ac = 
            WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
            
        LongProcessInvoker lpi = new LongProcessInvoker();
        lpi.setApplicationContext(ac);
            
        Executor ex = Executors.newSingleThreadExecutor();
        ex.execute(lpi);
    }
    catch(...) {
        ...
    }
}

In LongProcessInvoker we obtain the Session using SessionFactoryUtils and bind it to the current thread. Next the long running process is invoked and once complete we execute some clean-up code by releasing and closing the Session.

// LongProcessInvoker.java
public void run() {
    try {
        // Bind session object. 
        SessionFactory sessionFactory = 
            (SessionFactory) applicationContext.getBean("sessionFactory");
        Session session = SessionFactoryUtils.getSession(sessionFactory, true);
        TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session));        
        
        LongProcess lp = (LongProcess) applicationContext.getBean("longProcess");
        lp.runLongProcess();
        
        // Release session object.
        session.flush();
        TransactionSynchronizationManager.unbindResource(sessionFactory);
        SessionFactoryUtils.closeSession(session);
    }
    catch(...) {
        ...
    }
}

Using the above approach, we can now run long-running processes by obtaining the Hibernate session outside the view.

No comments: