11 Replies Latest reply on May 21, 2006 11:14 AM by itsmeprash

    Spring + jBPM 3.1 + Single Hibernate Session

    criess

      On 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!

        • 1. Re: Spring + jBPM 3.1 + Single Hibernate Session
          icyjamie

          see also http://www.jboss.com/index.html?module=bb&op=viewtopic&t=76546, but I guess your version is more elaborated.

          • 2. Re: Spring + jBPM 3.1 + Single Hibernate Session
            tom.baeyens

            interested in contributing this to jBPM ?

            you benefit because we will review and keep it up-to-date. we benefit from the extra spring support.

            if you're interested, please, post a message on the developers forum: http://www.jboss.org/index.html?module=bb&op=viewforum&f=219

            regards, tom.

            • 3. Re: Spring + jBPM 3.1 + Single Hibernate Session
              costin

              Not sure if you are aware but there is already jbpm integration code available as part of Spring Modules (springmodules.dev.java.net).
              Jbpm 3.1 has been already integrated but needs more tests - the code will be committed in the upcoming days.
              See the forums for more information : http://forum.springframework.org/forumdisplay.php?f=37

              • 4. Re: Spring + jBPM 3.1 + Single Hibernate Session
                costin

                I forgot to mention some details:
                The current code allows actions to be declared and injected from Spring application context. It is possible to use either a Spring application context or a configuration file for your jbpm configuration.

                One nice feature is that it integrates with Spring transaction support.
                You can find the current jbpm 3.0 code here: http://cvs.sourceforge.net/viewcvs.py/springframework/spring-projects/spring-jbpm/

                In the upcoming days the jbpm 3.0 and 3.1 integration code will be moved to Spring Modules.

                • 5. Re: Spring + jBPM 3.1 + Single Hibernate Session
                  icyjamie

                  criess,

                  what will happen if, from within the jbpm code, a call to JbpmConfiguration.getInstance() is made? Will you end up with the same configuration as defined in spring?

                  James

                  • 6. Re: Spring + jBPM 3.1 + Single Hibernate Session
                    rosenjiang

                    Is assignId(Object o) method could save "processInstance"?

                    Thanks

                    • 7. Re: Spring + jBPM 3.1 + Single Hibernate Session
                      criess

                      icyjamie,

                      No if you call JbpmConfiguration.getInstance() you will get a cofiguration instance other than that in Spring. Therefore, you want to use the application context with Spring. Here is an example of using Spring and jBPM:

                      
                      public class MyTest extends TestCase {
                      
                       private JbpmConfiguration jbpmConfiguration;
                      
                       protected void setUp() throws Exception {
                       super.setUp();
                      
                       jbpmConfiguration = (JbpmConfiguration) ctx
                       .getBean("jbpmConfiguration");
                      
                       }
                      
                       public void testSaveProcessInstance() {
                      
                       JbpmContext jbpmContext = null;
                      
                       jbpmContext = jbpmConfiguration.createJbpmContext();
                      
                       ProcessInstance processInstance = jbpmContext
                       .newProcessInstance("Proposal");
                      
                       Token token = processInstance.getRootToken();
                       assertEquals("start", token.getNode().getName());
                      
                       token.signal();
                       // Now the process is in the 'draft' state.
                       assertEquals("Draft", token.getNode().getName());
                      
                       // And close the jbpmSession.
                       jbpmContext.close();
                      
                       }
                      
                      }
                      


                      • 8. Re: Spring + jBPM 3.1 + Single Hibernate Session
                        criess

                        Also,

                        You want to make sure your application context create an individual jbpmContext and not a singleton. So the new application context would be this:

                        <beans>
                        
                         <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
                         <property name="dataSource">
                         <ref bean="dataSource" />
                         </property>
                         <property name="mappingJarLocations">
                         <list>
                         <value>classpath: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" singleton="false">
                         <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>


                        • 9. Re: Spring + jBPM 3.1 + Single Hibernate Session
                          terribo

                          org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager' defined in ServletContext resource [/WEB-INF/ApplicationContext-common.xml]: Can't resolve reference to bean 'sessionFactory' while setting property 'sessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory' defined in ServletContext resource [/WEB-INF/ApplicationContext-hibernate.xml]: Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: Mapping directory location [URL [file:war/WEB-INF/lib/jbpm-3.1.jar]] does not denote a directory

                          how to handle this error?

                          thanks

                          • 10. Re: Spring + jBPM 3.1 + Single Hibernate Session
                            itsmeprash

                            I am trying to work with Tomcat + Struts + Spring + JBPM.

                            I am trying this simple thing and it fails. The context fails to start with an error "startup failed due to previous errors".

                            If i comment just the reference to the hbm.xml file my context works fine.

                            
                            <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
                             <property name="dataSource"><ref bean="dataSource"/></property>
                             <property name="mappingResources">
                             <list>
                             <value>org/jbpm/msg/Message.hbm.xml</value>
                             </list>
                             </property>
                             <property name="hibernateProperties">
                             <props>
                             <prop key="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</prop>
                             </props>
                             </property>
                             </bean>
                            
                            


                            thanks in advance
                            Prash

                            • 11. Re: Spring + jBPM 3.1 + Single Hibernate Session
                              itsmeprash

                              Sorry my mistake. So sad that i did not think Message.hbm.xml would be looking for other files coz., it does a join on another table.

                              Once i mentioned all the hbm files it works now.

                              Thanks
                              Prash