Spring integration in jBPM 4
From Tomek Bujok's IT Notepad
I would like to show here how to integrate jBPM with Spring and Maven2 in order to create a robust BPM application stack. I have seen that there are many questions concerning that issue, so I would like to clarify and systematize it a bit.
Contents |
Prerequisites / Technologies used
- jBPM v.4.2
- Spring v.2.5.6.SEC01
- Maven 2.0.9
- JDK 1.6.0_17
Maven Project Configuration [pom.xml]
First, we have to create a sample Maven project.
mvn archetype:create -DgroupId=info.bujok.tomek.jbpm.spring -DartifactId=jbpm-spring-integration
Our Maven project is ready to go. Now, delete App.java and AppTest.java files generated during project creation. Then, we have to edit pom.xml in order to add proper entries. The most important sections are the ones with jBPM and Spring dependencies. The whole pom.xml file should look like this:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>info.bujok.tomek.jbpm.spring</groupId> <artifactId>jbpm-spring</artifactId> <packaging>jar</packaging> <name>jBPM + Spring Evaluation Project</name> <version>1.0</version> <properties> <jbpm.version>4.2</jbpm.version> <spring.version>2.5.6.SEC01</spring.version> <hsqldb.version>1.8.0.10</hsqldb.version> <junit.version>4.7</junit.version> <log4j.version>1.2.15</log4j.version> <hibernate.core.version>3.3.1.GA</hibernate.core.version> <hibernate.entitymanager.version>3.4.0.GA</hibernate.entitymanager.version> <hibernate.annotations.version>3.4.0.GA</hibernate.annotations.version> <hibernate.validator.version>3.1.0.GA</hibernate.validator.version> <commons.lang.version>2.4</commons.lang.version> <commons.logging.version>1.1.1</commons.logging.version> <cglib.version>2.2</cglib.version> </properties> <dependencies> <!-- JBPM dependencies --> <dependency> <groupId>org.jbpm.jbpm4</groupId> <artifactId>jbpm-jpdl</artifactId> <version>${jbpm.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.core.version}</version> <exclusions> <exclusion> <groupId>xml-apis</groupId> <artifactId>xml-apis</artifactId> </exclusion> <exclusion> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>${hibernate.annotations.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.entitymanager.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>${hibernate.validator.version}</version> </dependency> <!-- DB dependencies --> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>${hsqldb.version}</version> </dependency> <!-- SPRING dependencies --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-agent</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>${cglib.version}</version> </dependency> <!-- utilities --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>${commons.lang.version}</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>${commons.logging.version}</version> <type>jar</type> <exclusions> <exclusion> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> <type>jar</type> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <type>jar</type> </dependency> </dependencies> <repositories> <repository> <id>maven2-repository.dev.java.net</id> <name>Java.net Repository for Maven</name> <url>http://download.java.net/maven/2/</url> <layout>default</layout> </repository> <repository> <id>jboss-repository</id> <name>JBoss Repository</name> <url>http://repository.jboss.com/maven2/</url> <layout>default</layout> </repository> </repositories> </project>
Logging configuration [log4j.xml]
In order to trace the behavior of our system logging has to be configured. Config file should be located in the resources folder (src/main/resources or src/test resources). Here's a sample logging setup:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%-5p | %-10t | %-15.15c{1} | %-30.30C %4L | %m%n" /> </layout> </appender> <logger name="info.bujok.tomek"> <level value="TRACE" /> <appender-ref ref="CONSOLE" /> </logger> <logger name="org.jbpm"> <level value="INFO" /> <appender-ref ref="CONSOLE" /> </logger> <logger name="org.springframework"> <level value="WARN" /> <appender-ref ref="CONSOLE" /> </logger> <logger name="org.hibernate"> <level value="WARN" /> <appender-ref ref="CONSOLE" /> </logger> </log4j:configuration>
jBPM Configuration [jbpm.cfg.xml]
Next, we have to create a Spring-aware jBPM configuration file. It should be also located in the resources folder. Content of the file with essential comments in-line is outlined below:
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?> <jbpm-configuration xmlns="http://jbpm.org/xsd/cfg"> <import resource="jbpm.jpdl.cfg.xml" /> <import resource="jbpm.identity.cfg.xml" /> <import resource="jbpm.jobexecutor.cfg.xml" /> <process-engine-context> <repository-service /> <repository-cache /> <execution-service /> <history-service /> <management-service /> <identity-service /> <task-service /> <command-service name="newTxRequiredCommandService"> <retry-interceptor /> <environment-interceptor policy="requiresNew" /> <spring-transaction-interceptor /> </command-service> <command-service name="txRequiredCommandService"> <retry-interceptor /> <environment-interceptor /> <spring-transaction-interceptor /> </command-service> <object class="org.jbpm.pvm.internal.id.DatabaseDbidGenerator"> <field name="commandService"> <ref object="txRequiredCommandService" /> </field> </object> <object class="org.jbpm.pvm.internal.id.DatabaseIdComposer" init="eager"> </object> <!-- Added spring as read-context --> <script-manager default-expression-language="juel" default-script-language="juel" read-contexts="execution, environment, process-engine, spring" write-context=""> <script-language name="juel" factory="org.jbpm.pvm.internal.script.JuelScriptEngineFactory" /> </script-manager> <authentication /> <id-generator /> <types resource="jbpm.variable.types.xml" /> <address-resolver /> </process-engine-context> <transaction-context> <repository-session /> <db-session /> <message-session /> <timer-session /> <history-session /> <!-- Need to set explicitly that we don't want jbpm to create sessions --> <hibernate-session current="true" close="false" /> </transaction-context> </jbpm-configuration>
Spring Configuration [applicationContext.xml]
To finish the configuration process we have to create a Spring application context. When it comes to the analyzed issue there are two crucial sections of the file that should be taken into account:
- definition of the sessionFactory bean that contains a list of the jBPM hbm.xml files;
- definition of the jbpmConfiguration bean which contains the name of the jbpm configuration file as a constructor parameter;
Get acquainted with the factory-method design pattern sicne jBPM uses it a lot. jbpmConfiguration bean is a factory of processEngine, which is a factory of all jBPM services (such as taskService, executionService, etc.)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:applicationContext.properties" /> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="mappingLocations"> <list> <value>classpath:jbpm.execution.hbm.xml</value> <value>classpath:jbpm.repository.hbm.xml</value> <value>classpath:jbpm.task.hbm.xml</value> <value>classpath:jbpm.history.hbm.xml</value> <value>classpath:jbpm.identity.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop> <prop key="hibernate.query.substitutions">true=1 false=0</prop> <prop key="hibernate.bytecode.use_reflection_optimizer">false</prop> <prop key="hibernate.show_sql">false</prop> <prop key="hibernate.format_sql">false</prop> <prop key="hibernate.use_sql_comments">false</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> </props> </property> <property name="dataSource"> <ref bean="dataSource" /> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> <property name="url" value="${db.url}" /> <property name="username" value="${db.username}" /> <property name="password" value="${db.password}" /> </bean> <bean id="jbpmConfiguration" class="org.jbpm.pvm.internal.cfg.SpringConfiguration"> <constructor-arg value="jbpm.cfg.xml" /> </bean> <bean id="processEngine" factory-bean="jbpmConfiguration" factory-method="buildProcessEngine"> </bean> <bean id="executionService" factory-bean="processEngine" factory-method="getExecutionService"> </bean> <bean id="repositoryService" factory-bean="processEngine" factory-method="getRepositoryService"> </bean> <bean id="taskService" factory-bean="processEngine" factory-method="getTaskService"> </bean> <bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService"> </bean> <bean id="identityService" factory-bean="processEngine" factory-method="getIdentityService"> </bean> <bean id="managementService" factory-bean="processEngine" factory-method="getManagementService"> </bean> </beans>
As you have already noticed some of the configuration values are externalized to the properties file (applicationContext.properties) which looks like this:
db.url=jdbc:hsqldb:mem:. db.username=sa db.password=
Now, we are ready to create a sample test case!
Sample Test Case
Right here, we have a very simple test scenario, which:
- defines and deploys a sample jPDL business process (sample process definition was taken from jBPM examples).
- starts a sample process instance
- executes all steps of the process - checking if the process flow is routed as expected.
package info.bujok.tomek.jbpm.spring; import org.jbpm.api.Execution; import org.jbpm.api.ProcessInstance; import org.jbpm.test.AbstractTransactionalSpringJbpmTestCase; /** * @author Tomasz Bujok (tom[dot]bujok[at]gmail[dot]com) * */ public class JbpmTestCase extends AbstractTransactionalSpringJbpmTestCase { private String processName; private String processDefinition; @Override protected String[] getConfigLocations() { return new String[] { "applicationContext.xml" }; } @Override protected void onSetUpInTransaction() throws Exception { super.onSetUpInTransaction(); processName = "StateSequence"; processDefinition = "<process name='StateSequence'>"+ " <start>"+ " <transition to='a'/>"+ " </start>"+ " <state name='a'>"+ " <transition to='b'/>"+ " </state>"+ " <state name='b'>"+ " <transition to='c'/>"+ " </state>"+ " <state name='c'/>"+ "</process>"; deployJpdlXmlString(processDefinition); } public void testWaitStatesSequence() { ProcessInstance processInstance = null; processInstance = executionService.startProcessInstanceByKey(processName); Execution execInA = processInstance.findActiveExecutionIn("a"); assertNotNull(execInA); processInstance = executionService.signalExecutionById(execInA.getId()); Execution execInB = processInstance.findActiveExecutionIn("b"); assertNotNull(execInB); processInstance = executionService.signalExecutionById(execInB.getId()); Execution execInC = processInstance.findActiveExecutionIn("c"); assertNotNull(execInC); } }
Result
Finally, the project structure looks like this:
└───jbpm-spring-integration
│ pom.xml
│
└───src
└───test
├───java
│ └───info
│ └───bujok
│ └───tomek
│ └───jbpm
│ └───spring
│ JbpmTestCase.java
│
└───resources
applicationContext.properties
applicationContext.xml
jbpm.cfg.xml
log4j.xml
If you run:
mvn test
in the main project folder you will see that everything is fine:
[...] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.547 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------ [INFO] Total time: 7 seconds [INFO] Finished at: Tue Dec 15 11:18:32 CET 2009 [INFO] Final Memory: 8M/254M [INFO] ------------------------------------------------------------------------
Download
Comming soon... I will publish the code on github and googlecode.
Summary
Few simple steps described above were enough to setup a complete jBPM + Spring application stack. Isn't that beautiful :) Hope this helps!
