ajeydudhe / workflow-lite

Simple lightweight workflow framework using Spring beans.

Home Page:http://www.ajeydudhe.com/2017/10/simple-sequential-workflow-using-spring.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Build Status Maven metadata URI MIT licensed

Workflow-lite - Simple workflow engine using Spring framework and Spring Expression Language

Workflow-lite is a simple workflow engine using the Spring framework. As of now, it can be used to define a simple sequential workflow.

  • A workflow consists of Actions to be executed in the given order.
  • An action is a class performing a unit of work.
  • An action can be defined as normal Spring bean with required dependencies injected.
  • Apart from this the output of one action can be injected into the other action.
  • Supports conditional flow.
  • Supports asynchronous execution.
  • Using the Spring Expression Language one can inject original source, output of previous action, properties from execution context etc. into the action to be instantiated.
  • The workflow can be defined using UML2 activity diagram.

How is it different?

There are few blogs on how to use Spring to have a simple sequential workflow. But they mostly deal with sequential action execution without support for conditional branching. Also, in most cases, the interface for performing action takes some context which is used to pass the inputs from one action to another which makes the actions interdependent.

Workflow-lite on the other hand allows to define the actions as normal Java classes defining their dependencies to be injected using constructor or properties. Even the output of one action can be passed to another using dependency injection and not using the context object.

Prerequisites

  • Maven for building the project.
  • Papyrus eclipse plug-in for defining the workflow using UML activity diagram.

Adding the library reference

Add the maven dependency to your pom.xml as follows.

<dependency>
    <groupId>org.expedientframework.workflowlite</groupId>
    <artifactId>workflow-lite-core</artifactId>
    <version>1.0.0</version>
</dependency>

Use case - Student score card preparation

We will define a workflow to calculate the score for a given student. The workflow will take a student object as input and have following actions:

  • CalculateTotalScoreAction - Takes map of subject to marks as input and returns total of all the marks.
  • AddBonusMarksAction - Takes the total score for a student and adds 10 bonus marks.
  • PublishStudentScoreAction - Takes student name and total score as input and returns a simple string describing the score e.g. Student 'John Doe' scored 130 marks.

Defining the workflow using UML activity diagram

Using the Papyrus plugin create the activity diagram as follow:

Workflow

  • Add the Opaque action node to represent the workflow actions.
  • Add the Decision node to represent the condition. Since Papyrus does not show the name of condition use the comment to call out the condition.

Implementing the actions

All the workflow actions needs to implement the Action interface. Also, instead of directly implementing the interface consider extending the AbstractAction or AbstractAsyncAction as follows:

public class PublishStudentScoreAction extends AbstractAction<ExecutionContext, String>
{
  public PublishStudentScoreAction(final String studentName, final int score)
  {
    this.studentName = studentName;
    this.score = score;
  }
  
  @Override
  public String execute(final ExecutionContext context)
  {
    return String.format("Student '%s' scored %d marks.", this.studentName, this.score);
  }
  
  // Private
  private final String studentName;
  private final int score;
}

As seen above, the PublishStudentScoreAction simply takes the student name and score as constructor parameters and then in execute() returns a simple formatted string. Note that we are not using ExecutionContext object to pass parameters to actions but using constructor injection. Similarly, implement other actions.

Linking the UML activity diagram with implementation

So far we have created UML activity diagram describing the workflow we need to execute and implemented the actions. But how do we link them? This is also a very simple steps. We will use String dependency injection here and define the workflow actions as beans.

  • Go to the activity diagram and select an Opaque node.
  • In the properties view click the UML tab.
  • Select Add option on Language.
  • Select JAVA as language and click Ok.
  • On the right hand side paste the Spring bean definition for the class. For example, following bean is for CalculateTotalScoreAction action.
     <bean class="org.expedientframework.workflowlite.core.samples.CalculateTotalScoreAction">
     	<constructor-arg value="%{student.scores}" />
     </bean>

In above bean definition we have used Spring Expression Language to pass the input. Our expression definition starts with %{ and ends with }. For this example, we are passing the value of property stores on the student object. The student object is our input to the workflow. In our StudentWorkflowExecutionContext we have mentioned that the input to the workflow should be referred as student in the expressions. Also, there are context and output variables available. context refers to the ExecutionContext instance while output refers to the output from previous action. The PublishStudentScoreAction takes two inputs: one from original input referred to as student.name in the expression below and other from previous activity referred to as output below. The student variable refers to Student instance while output is a simple numeric value.

<bean class="org.expedientframework.workflowlite.core.samples.PublishStudentScoreAction">
	<constructor-arg name="studentName" value="%{student.name}" />
	<constructor-arg name="score" value="%{output}" />
</bean>

Defining the condition

To define the conditional flow we will again use the Spring expression.

  • Go to the activity diagram and select a Decision node.
  • Here, we will put the Spring expression as name of the node.
  • For this example, we use the expression as %{context.completedExtracurricularActivities(student.getName())}
  • Above expression states that invoke the completedExtracurricularActivities method on the context instance passing in the student name.
  • The Spring expression result is then used to determine which flow to execute.
  • For this to happen, we need to select the outgoing links and name them as per the expected output from expression.
  • In our example, the outgoing links from decision node has name as true and false since our expression returns these values. Note that we will always do toString() on the expression result to match the outgoing link names.

Registering the workflow

Now that we have implemented the actions and also linked them with the given classes, it's time to register the workflows by implementing the WorkflowDefinitionsProvider interface. When the application starts, it checks all the beans implementing this interface and invoke them one by one. The interface definition is simple as follows:

public interface WorkflowDefinitionsProvider
{
  public List<InputStream> getDefinitions();
}

It just expects the list of streams of UML files having the workflow definitions. The UmlActivityDefinitionsProvider implements the above interface. It simply takes the file names as input and then will resolve them and return the list of streams. In your application bean, add the following bean definition:

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<import resource="spring-beans/workflow-lite-core.xml"/>
	
	<bean id="workflowDefinitions" class="org.expedientframework.workflowlite.core.UmlActivityDefinitionsProvider">
		<constructor-arg>
			<list>
				<value>classpath:workflows/workflow_definitions.uml</value>
			</list>
		</constructor-arg>
	</bean>
</beans>

In above sample, we are including the workflow-lite-core.xml which has the required framework beans defined. Then we have the UmlActivityDefinitionsProvider taking the list of files having the UML definitions. For this example, our UML definitions are in workflow_definitions.uml. You can use classpath or filepath.

Executing the workflows

The StudentScoreCardWorkflowTest shows how we are going to execute the workflow. We are using TestNG to write the tests as follows:

@ContextConfiguration(locations="classpath:wf_definitions.xml")
public class StudentScoreCardWorkflowTest extends AbstractTestNGSpringContextTests 
{
  @Test
  public void resultForNormalStudent_resultWithoutBonusMarks() 
  {
    final Student student = new Student("John Doe");
    student.addScore("History", 60);
    student.addScore("Science", 70);
    
    final String result = this.workflowManager.execute(new StudentWorkflowExecutionContext(), student);
    assertThat(result).as("Result").isEqualTo("Student 'John Doe' scored 130 marks.");
  }

  @Test
  public void resultForNormalStudent_resultWithBonusMarks() 
  {
    final Student student = new Student("Octavia Wilford");
    student.addScore("History", 60);
    student.addScore("Science", 70);
    
    final String result = this.workflowManager.execute(new StudentWorkflowExecutionContext(), student);
    assertThat(result).as("Result").isEqualTo("Student 'Octavia Wilford' scored 140 marks.");
  }
  
  // Private
  @Inject
  private WorkflowManager workflowManager;
}

The @ContextConfiguration points to the bean XML wf_definitions.xml to be used which imports the workflow-lite beans and specifies the path to the UML definition file as mentioned in previous section. Next we inject the instance of WorkflowManager. The two tests are simple. They create the student instance, adds the marks and then execute the workflow passing the student instance. The logic to detemine whether student has participated in any extracurricular activity or not is simple. If the student name starts with a vowel (aeiou) then we return true else false. So the first test with student name as John Doe will not have any bonus marks added while the second test with student name as Octavia Wilford will have the bonus marks added.

Also, notice that we are passing the instance of StudentWorkflowExecutionContext. The constructor of this class takes the workflow id as input which should be the name of the UML activity and the alias to be used for refering the input which in this case is student since we are passing student instance. The alias is used in the Spring expressions for passing the inputs to actions or evaluating the condition.

Asynchronous execution

In most of the cases an action will perform some asynchronous operation or will wait on some other asynchronous operation to complete. Hence, the overall workflow execution itself needs to be asynchronous. Handling this is very easy. The action needs to simply return a CompletableFuture and that's all. The output from the workflow itself will be a CompletableFuture and consumer should use appropriate methods on it to listen for result or error.

Work in progress

  • Optimize the expression evaluation by caching the expressions.
  • Error handling.
  • Persistence support for workflows.