Spring + jBPM 3.1 + Single Hibernate Session
criess Feb 23, 2006 7:56 PMOn my web project we are using Spring, jBPM 3.1, and Hibernate. We want our Hibernate domain objects and jBPM's objects to run under the same Hibernate Session and transaction. The key here is that if an error occurs during the web request we want to rollback any jBPM database actions along with any of our our Hibernate domain objects.
What follows is the steps I used to accomplish this. This is provided to teach others that want to do the same. Feel free to point out if this could have been done easier.
Step 1: Create a custom object factory
package org.donorschoose.workflow; import org.jbpm.JbpmConfiguration; import org.jbpm.configuration.ObjectFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; public class JbpmSpringObjectFactory implements ObjectFactory, ApplicationContextAware { private static final long serialVersionUID = 1L; private ApplicationContext context; public JbpmSpringObjectFactory () { JbpmConfiguration.Configs.setDefaultObjectFactory(this); } public Object createObject(String name) { return context.getBean(name); } public boolean hasObject(String name) { return context.containsBean(name); } public void setApplicationContext(ApplicationContext context) { this.context = context; } }
Step 2: Create a Spring application context file that will initialize jBPM via a custom object factory.
<beans> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="mappingJarLocations"> <list> <value>file:war/WEB-INF/lib/jbpm-3.1.jar</value> </list> </property> ... </bean> <bean id="jbpmObjectFactory" class="org.donorschoose.workflow.JbpmSpringObjectFactory"> </bean> <bean id="dbService" class="org.donorschoose.workflow.SpringDbPersistenceServiceFactory"> <property name="sessionFactory"> <ref local="sessionFactory" /> </property> </bean> <bean id="jbpmConfiguration" class="org.jbpm.JbpmConfiguration"> <constructor-arg index="0" ref="jbpmObjectFactory"/> </bean> <bean id="services" class="org.jbpm.svc.Services"> <constructor-arg index="0"> <map> <entry key="persistence"> <ref local="dbService"/> </entry> </map> </constructor-arg> </bean> <bean id="default.jbpm.context" class="org.jbpm.JbpmContext"> <constructor-arg index="0" ref="services"/> <constructor-arg index="1" ref="jbpmObjectFactory"/> </bean> <bean id="resource.default.modules" class="java.lang.String"> <constructor-arg value="org/jbpm/graph/def/jbpm.default.modules.properties"/> </bean> <bean id="resource.action.types" class="java.lang.String"> <constructor-arg value="org/jbpm/graph/action/action.types.xml"/> </bean> <bean id="resource.node.types" class="java.lang.String"> <constructor-arg value="org/jbpm/graph/node/node.types.xml"/> </bean> <bean id="resource.varmapping" class="java.lang.String"> <constructor-arg value="org/jbpm/context/exe/jbpm.varmapping.xml"/> </bean> </beans>
Step 3: Create a custom DbServiceFactory
package org.donorschoose.workflow; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jbpm.persistence.db.DbPersistenceServiceFactory; import org.jbpm.svc.Service; public class SpringDbPersistenceServiceFactory extends DbPersistenceServiceFactory { private static final long serialVersionUID = 1L; public Service openService() { log.debug("creating persistence service"); return new SpringDbPersistenceService(this); } private static Log log = LogFactory .getLog(SpringDbPersistenceServiceFactory.class); }
Step 4: Create a custom DbPersistenceService
package org.donorschoose.workflow; import java.sql.Connection; import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.springframework.orm.hibernate3.SessionFactoryUtils; import org.jbpm.JbpmException; import org.jbpm.db.ContextSession; import org.jbpm.db.GraphSession; import org.jbpm.db.LoggingSession; import org.jbpm.db.MessagingSession; import org.jbpm.db.SchedulerSession; import org.jbpm.db.TaskMgmtSession; import org.jbpm.persistence.JbpmPersistenceException; import org.jbpm.persistence.PersistenceService; import org.jbpm.svc.Service; public class SpringDbPersistenceService implements Service, PersistenceService { private static final long serialVersionUID = 1L; SpringDbPersistenceServiceFactory persistenceServiceFactory = null; Connection connection = null; boolean mustConnectionBeClosed = false; Transaction transaction = null; boolean isTransactionEnabled = true; boolean isRollbackOnly = false; Session session; boolean mustSessionBeFlushed = false; boolean mustSessionBeClosed = false; GraphSession graphSession = null; TaskMgmtSession taskMgmtSession = null; SchedulerSession schedulerSession = null; MessagingSession messagingSession = null; ContextSession contextSession = null; LoggingSession loggingSession = null; public SpringDbPersistenceService(SpringDbPersistenceServiceFactory persistenceServiceFactory) { this.persistenceServiceFactory = persistenceServiceFactory; this.isTransactionEnabled = persistenceServiceFactory.isTransactionEnabled(); } public SessionFactory getSessionFactory() { return persistenceServiceFactory.getSessionFactory(); } public Session getSession() { if ( (session==null) && (getSessionFactory()!=null) ) { Connection connection = getConnection(false); if (connection!=null) { log.debug("creating hibernate session with connection "+connection); session = getSessionFactory().openSession(connection); mustSessionBeClosed = true; mustSessionBeFlushed = true; mustConnectionBeClosed = false; } else { log.debug("creating hibernate session using SessionFactoryUtils"); session = SessionFactoryUtils.getSession(getSessionFactory(), true); mustSessionBeClosed = true; mustSessionBeFlushed = false; mustConnectionBeClosed = false; } isTransactionEnabled = !SessionFactoryUtils.isSessionTransactional(session, getSessionFactory()); if (isTransactionEnabled) { log.debug("beginning hibernate transaction"); transaction = session.beginTransaction(); } } return session; } public Connection getConnection() { return getConnection(true); } Connection getConnection(boolean resolveSession) { if (connection==null) { if (persistenceServiceFactory.getDataSource()!=null) { try { log.debug("fetching jdbc connection from datasource"); connection = persistenceServiceFactory.getDataSource().getConnection(); mustConnectionBeClosed = true; } catch (Throwable t) { throw new JbpmException("couldn't obtain connection from datasource", t); } } else { if (resolveSession) { // initializes the session member getSession(); } if (session!=null) { log.debug("fetching connection from hibernate session. this transfers responsibility for closing the jdbc connection to the user!"); connection = session.connection(); mustConnectionBeClosed = false; } } } return connection; } public void close() { if ( (session!=null) && (transaction==null) && (isRollbackOnly) ) { throw new JbpmException("User provided session was combined with setRollbackOnly. With user provided hibernate sessions, the user is responsible for managing transactions"); } if (messagingSession!=null) { messagingSession.closeOpenIterators(); } if (schedulerSession!=null) { schedulerSession.closeOpenIterators(); } if ( (isTransactionEnabled) && (transaction!=null) ) { if (isRollbackOnly) { try { log.debug("rolling back hibernate transaction"); mustSessionBeFlushed = false; // flushing updates that will be rolled back is not very clever :-) transaction.rollback(); } catch (Throwable t) { throw new JbpmPersistenceException("couldn't rollback hibernate session", t); } } else { try { log.debug("committing hibernate transaction"); mustSessionBeFlushed = false; // commit does a flush anyway transaction.commit(); } catch (Throwable t) { try { // if the commit fails, we must do a rollback transaction.rollback(); } catch (Throwable t2) { // if the rollback fails, we did what we could and you're in // deep shit :-( log.error("problem rolling back after failed commit", t2); } throw new JbpmPersistenceException("couldn't commit hibernate session", t); } } } if (mustSessionBeFlushed) { try { log.debug("flushing hibernate session"); session.flush(); } catch (Throwable t) { throw new JbpmPersistenceException("couldn't flush hibernate session", t); } } if (mustSessionBeClosed) { try { log.debug("closing hibernate session using SessionFactoryUtils"); SessionFactoryUtils.releaseSession(session, getSessionFactory()); } catch (Throwable t) { throw new JbpmPersistenceException("couldn't close hibernate session", t); } } if (mustConnectionBeClosed) { try { log.debug("closing jdbc connection"); connection.close(); } catch (Throwable t) { throw new JbpmPersistenceException("couldn't close jdbc connection", t); } } } public void assignId(Object object) { try { getSession().save(object); } catch (Throwable t) { throw new JbpmPersistenceException("couldn't assign id to "+object, t); } } // getters and setters ////////////////////////////////////////////////////// public GraphSession getGraphSession() { if (graphSession==null) { Session session = getSession(); if (session!=null) { graphSession = new GraphSession(session); } } return graphSession; } public LoggingSession getLoggingSession() { if (loggingSession==null) { Session session = getSession(); if (session!=null) { loggingSession = new LoggingSession(session); } } return loggingSession; } public MessagingSession getMessagingSession() { if (messagingSession==null) { Session session = getSession(); if (session!=null) { messagingSession = new MessagingSession(session); } } return messagingSession; } public SchedulerSession getSchedulerSession() { if (schedulerSession==null) { Session session = getSession(); if (session!=null) { schedulerSession = new SchedulerSession(session); } } return schedulerSession; } public ContextSession getContextSession() { if (contextSession==null) { Session session = getSession(); if (session!=null) { contextSession = new ContextSession(session); } } return contextSession; } public TaskMgmtSession getTaskMgmtSession() { if (taskMgmtSession==null) { Session session = getSession(); if (session!=null) { taskMgmtSession = new TaskMgmtSession(session); } } return taskMgmtSession; } public DataSource getDataSource() { return persistenceServiceFactory.getDataSource(); } public boolean isRollbackOnly() { return isRollbackOnly; } public void setRollbackOnly(boolean isRollbackOnly) { this.isRollbackOnly = isRollbackOnly; } public void setRollbackOnly() { isRollbackOnly = true; } public void setSession(Session session) { this.session = session; log.debug("injecting a session disables transaction"); isTransactionEnabled = false; } public void setConnection(Connection connection) { this.connection = connection; } public void setContextSession(ContextSession contextSession) { this.contextSession = contextSession; } public void setDataSource(DataSource dataSource) { this.persistenceServiceFactory.setDataSource (dataSource); } public void setGraphSession(GraphSession graphSession) { this.graphSession = graphSession; } public void setLoggingSession(LoggingSession loggingSession) { this.loggingSession = loggingSession; } public void setMessagingSession(MessagingSession messagingSession) { this.messagingSession = messagingSession; } public void setSchedulerSession(SchedulerSession schedulerSession) { this.schedulerSession = schedulerSession; } public void setTaskMgmtSession(TaskMgmtSession taskMgmtSession) { this.taskMgmtSession = taskMgmtSession; } public void setSessionFactory(SessionFactory sessionFactory) { this.persistenceServiceFactory.setSessionFactory (sessionFactory); } public Transaction getTransaction() { return transaction; } public void setTransaction(Transaction transaction) { this.transaction = transaction; } public boolean isTransactionEnabled() { return isTransactionEnabled; } public void setTransactionEnabled(boolean isTransactionEnabled) { this.isTransactionEnabled = isTransactionEnabled; } private static Log log = LogFactory.getLog(SpringDbPersistenceService.class); }
Now we wrap each web request in a transaction. We open the transaction at the start of the web request and close the transaction at the end of the request. Again if we encounter any errors while processing the request all database actions are rolled back.
All the magic happens using Spring's SessionFactoryUtils class in the custom DbPersistenceService.
Again, if anybody has a better way of doing this please respond.
Thanks!