I have been trying to model a workflow for one of my applications. I started with Bossa, but after getting stuck using parts of its API, I was well on my way to designing my own workflow engine using Bossa's Petri Net abstraction, until a colleague pointed out that I was trying to re-invent something for which solid, mature implementations already existed in the open-source world, and that, down the road, other developers would curse me for having stuck them with having to maintain this component.
This week, I describe my experience implementing a solution to my workflow problem using OSWorkflow, a popular and mature workflow engine developed by the OpenSymphony group. Actually, I was quite pleasantly surprised to see Hani Suleiman's name as the author of some of the classes. For those of you who don't know, he is also the author of the Bileblog. Although he doesn't write it anymore, its worth a read simply because of his ability to call bullshit on any number of things, and his ability to call a spade a...er...several very unmentionable things. I don't agree with a lot of the stuff he said, but it was a very entertaining (and sometimes informative) read nevertheless. Anyway, like I said, it was nice to see his name on the @author tags.
My initial take on OSWorkflow is that it is less "pure" in its abstraction, but probably closer to the way people think of a workflow in the real world. I show my workflow redrawn the OSWorkflow way below. Notice how similar it is to the picture of the back-of-the-envelope version of the workflow.
However, in trying to make OSWorkflow "user-friendly", the concept of Step (Place in Bossa) has been overloaded to include Action (Transition in Bossa). Because of this, there are extra constructs for Split and Join (indicated above by pink and blue), Initial Action to kick off a workflow, and Pre- and Post-Functions to "attach" to a Step (instead of being the input and output Transition to a Place as in Bossa). What's more, Splits have to be modeled as real Steps, while Joins don't. So for me there was some amount of unlearning to do. Another problem is that the documentation is rather sparse and a lot of what I did was by trial and error.
Also, my needs were slightly different from the standard usage. The workflow I was trying to build is a network of batch processes, which have dependencies on each other. The batch jobs will be run using JMS (more on that probably next week), so the application needs to be event-driven. That is, each of the Actions would call back the main Workflow Runner once it is done, so the runner can move the workflow forward.
I am using Spring for the rest of the application, so I planned on using Spring's event handling framework for this. OSWorkflow has built-in integration with Spring, which I used, but that does not expose the actual post-functions as Spring objects. Fortunately, I was able to find a solution in this springmodules page, which works even without having the osworkflow springmodules module in your classpath.
Configuration
Configuration is using Spring and a couple of XML files that are used to define the workflow to OSWorkflow. The Spring applicationContext.xml file is shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<bean id="workflowStore"
class="com.opensymphony.workflow.spi.memory.MemoryWorkflowStore"/>
<bean id="workflowFactory"
class="com.opensymphony.workflow.spi.hibernate.SpringWorkflowFactory"
init-method="init">
<property name="resource" value="workflow-defs.xml"/>
<property name="reload" value="true"/>
</bean>
<bean id="workflowConfiguration"
class="com.opensymphony.workflow.config.SpringConfiguration">
<property name="factory" ref="workflowFactory"/>
<property name="store" ref="workflowStore"/>
</bean>
<bean id="workflowTypeResolver"
class="com.opensymphony.workflow.util.SpringTypeResolver">
<property name="functions">
<map>
<entry key="mockAction"><ref bean="mockAction"/></entry>
</map>
</property>
</bean>
<bean id="workflow"
class="com.opensymphony.workflow.basic.BasicWorkflow" scope="prototype">
<constructor-arg><value>testuser</value></constructor-arg>
<property name="configuration" ref="workflowConfiguration"/>
<property name="resolver" ref="workflowTypeResolver"/>
</bean>
<bean id="mockAction" class="com.mycompany.myapp.workflow.MockAction" scope="prototype"/>
<bean id="workflowRunner"
class="com.mycompany.myapp.workflow.WorkflowRunner" scope="prototype">
<property name="workflow" ref="workflow"/>
</bean>
</beans>
|
Here all except the last two beans are needed by OSWorkflow to define a Store and Factory, which in turn defines the configuration for the workflow. The workflowTypeResolver specifies that certain classes are going to be recognized as Spring functions by the Workflow. The mockAction is our Action class which we use for tests. In the real application, there will be specialized Action classes that will be injected into the appropriate workflow Steps.
The WorkflowFactory is built using the osworkflow file, workflow-defs.xml, which include a list of other workflow files, one for each workflow defined in the system. Both these files need to be in the application classpath.
1 2 3 4 | <?xml version="1.0" encoding="UTF-8"?>
<workflows>
<workflow name="workflow-def-1" type="resource" location="workflow-def-1.xml"/>
</workflows>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow PUBLIC
"-//OpenSymphony Group//DTD OSWorkflow 2.8//EN"
"http://www.opensymphony.com/osworkflow/workflow_2_8.dtd">
<workflow>
<initial-actions>
<action id="0" name="start">
<pre-functions>
<function type="class">
<arg name="class.name">com.opensymphony.workflow.util.Caller</arg>
</function>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t0</arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="1"/>
</results>
</action>
</initial-actions>
<steps>
<step id="1" name="p1">
<actions>
<action id="1" name="t1">
<results>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="26"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t1</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="26" name="s26">
<actions>
<action id="26" name="split26">
<results>
<unconditional-result old-status="Finished" owner="${caller}" split="26"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">split26</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="2" name="p2">
<actions>
<action id="2" name="t2">
<results>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="34"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t2</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="34" name="s34">
<actions>
<action id="34" name="split34">
<results>
<unconditional-result old-status="Finished" owner="${caller}" split="34"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">split34</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="3" name="p3">
<actions>
<action id="3" name="t3">
<results>
<unconditional-result old-status="Finished" owner="${caller}" join="43"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t3</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="4" name="p4">
<actions>
<action id="4" name="t4">
<results>
<unconditional-result old-status="Finished" owner="${caller}" join="43"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t4</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="5" name="p5">
<actions>
<action id="5" name="t5">
<results>
<unconditional-result old-status="Finished" owner="${caller}" join="75"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t5</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="6" name="p6">
<actions>
<action id="6" name="t6">
<results>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="7"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t6</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="7" name="p7">
<actions>
<action id="7" name="t7">
<results>
<unconditional-result old-status="Finished" owner="${caller}" join="75"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t7</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="8" name="p8">
<actions>
<action id="8" name="t8">
<results>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="9"/>
</results>
<post-functions>
<function type="spring">
<arg name="bean.name">mockAction</arg>
<arg name="action.name">t8</arg>
</function>
</post-functions>
</action>
</actions>
</step>
<step id="9" name="stop">
<actions>
<action id="9" name="t9" finish="true">
<results>
<unconditional-result old-status="Finished"
status="Complete" owner="${caller}"/>
</results>
</action>
</actions>
</step>
</steps>
<splits>
<split id="26">
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="2"/>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="6"/>
</split>
<split id="34">
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="3"/>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="4"/>
</split>
</splits>
<joins>
<join id="43">
<conditions type="AND">
<condition type="beanshell">
<arg name="script"><![CDATA[
"Finished".equals(jn.getStep(3).getStatus()) &&
"Finished".equals(jn.getStep(4).getStatus())
]]>
</arg>
</condition>
</conditions>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="5"/>
</join>
<join id="75">
<conditions type="AND">
<condition type="beanshell">
<arg name="script"><![CDATA[
"Finished".equals(jn.getStep(5).getStatus()) &&
"Finished".equals(jn.getStep(7).getStatus())
]]>
</arg>
</condition>
</conditions>
<unconditional-result old-status="Finished" status="Queued"
owner="${caller}" step="8"/>
</join>
</joins>
</workflow>
|
The file above is the XML version of the workflow diagram above. It has approximately eight real steps, a start and stop step (which are pseudo steps in the sense that they don't do anything, but need to be modeled as regular steps here), two splits (each of which need to be modeled as steps) and two joins.
The reason the actual mockAction beans have been built as post-actions is because of the event based nature of the actions. Building them as pre-actions will result in an infinite loop because the workflow will never leave the first step. Since post-actions are called after the step is left, this is an ideal position to do this.
Event Subscriber and Publisher
The Event Subscriber is the WorkflowRunner class, which listens on events generated by the Action classes. The event contains the workflowId for the current Workflow instance, which is used to query the workflow to find the next available actions to execute. It implements Spring's ApplicationListener and exposes an onApplicationEvent which is triggered by events from the Actions. The code is shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | // WorkflowRunner.java
package com.mycompany.myapp.workflow;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import com.opensymphony.workflow.Workflow;
import com.opensymphony.workflow.WorkflowException;
import com.opensymphony.workflow.spi.Step;
public class WorkflowRunner implements ApplicationListener {
private final Log log = LogFactory.getLog(getClass());
private String workflowName;
private Workflow workflow;
private Map<String,Object> inputs;
private Set<Integer> alreadyExecuted = new HashSet<Integer>();
@Required
public void setWorkflow(Workflow workflow) {
this.workflow = workflow;
}
public void setWorkflowName(String workflowName) {
this.workflowName = workflowName;
}
public void setInputs(Map<String,Object> inputs) {
this.inputs = inputs;
}
public long init(int initialActionId) throws Exception {
long workflowId = workflow.initialize(workflowName, initialActionId, inputs);
return workflowId;
}
@SuppressWarnings({"unused","unchecked"})
public void process(long workflowId) {
List<Step> currentSteps = workflow.getCurrentSteps(workflowId);
for (Step currentStep : currentSteps) {
int[] availableActions = workflow.getAvailableActions(workflowId, inputs);
for (int availableAction : availableActions) {
if (alreadyExecuted.contains(availableAction)) {
continue;
}
try {
System.out.println("Executing action.id=" + availableAction);
workflow.doAction(workflowId, availableAction, inputs);
alreadyExecuted.add(availableAction);
} catch (WorkflowException e) {
log.error("Exception in (workflow,action.id)=(" + workflowName + "," +
availableAction + "). Workflow stopped", e);
}
}
}
}
public void onApplicationEvent(ApplicationEvent event) {
if (event.getSource() instanceof Long) {
process((Long) event.getSource());
}
}
}
|
The events are published into the ApplicationContext from our Action classes. The MockAction is only an example which prints the name of the action being executed. It implements FunctionProvider (so it can be called from within OSWorkflow as a function) and ApplicationContextAware (so it can publish events into Spring's ApplicationContext).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // MockAction.java
package com.mycompany.myapp.workflow;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.workflow.FunctionProvider;
import com.opensymphony.workflow.WorkflowException;
import com.opensymphony.workflow.spi.WorkflowEntry;
public class MockAction implements FunctionProvider, ApplicationContextAware {
private final Log log = LogFactory.getLog(getClass());
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* @see com.opensymphony.workflow.FunctionProvider#execute(
* java.util.Map, java.util.Map, com.opensymphony.module.propertyset.PropertySet)
*/
@SuppressWarnings({"serial","unchecked"})
public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException {
String actionName = (String) args.get("action.name");
WorkflowEntry workflowEntry = (WorkflowEntry) transientVars.get("entry");
long workflowId = workflowEntry.getId();
applicationContext.publishEvent(new ApplicationEvent(workflowId) {});
}
}
|
Example Client Code
Finally, here is how I plan to call the workflow. The workflow is kickstarted by a user request, however, any further movement in the workflow is done by the events, which call the WorkflowRunner.process method internally. Here is the JUnit test I used to test this out.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // WorkflowRunnerTest.java
package com.mycompany.myapp.workflow;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.opensymphony.workflow.Workflow;
public class WorkflowRunnerTest {
private WorkflowRunner runner;
private Map<String,Object> inputs = new HashMap<String,Object>();
private long workflowId;
@Before
public void setUp() throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {
"classpath:applicationContext.xml"
});
runner = (WorkflowRunner) context.getBean("workflowRunner");
runner.setWorkflowName("workflow-def-1");
runner.setInputs(inputs);
workflowId = runner.init(0);
}
@Test
public void testRunner() throws Exception {
runner.process(workflowId);
}
}
|
The output looks like this, which seems reasonable:
1 2 3 4 5 6 7 8 9 10 11 | Executing action.id=1
Executing action.id=26
Executing action.id=2
Executing action.id=6
Executing action.id=34
Executing action.id=7
Executing action.id=3
Executing action.id=4
Executing action.id=5
Executing action.id=8
Executing action.id=9
|
Conclusion
OSWorkflow seems to have quite a steep learning curve, but it is quite powerful once you understand how it all works. I don't know everything about it (not even close), and I am probably only going to explore more unless I have to. Like any other workflow engine, the hardest work is to design your workflow. During my searches for things OSWorkflow related, I did come across OSWorkflow: A guide for Java developers and architects, a book by Diego Naya, who also seems to be quite active on the OSWorkflow mailing list. I haven't read it, but it may be worth buying at some point in the future. OSWorkflow also comes with a GUI designer, which I did try in desperation at one point, but I could not do much with it, so I cannot comment on how good it is.












