Skip to main content

Simple Workflow Engine With Spring

Few months ago, during working on one of the company project, we had need to developed  REST services which is used for sending an email depending on data sent by client application. During developing this service we decide to create simple workflow engine which will be charged for sending an email, but also this engine can be used for any kind of simple flows.
In this article i will explain step by step how you can implement your simple workflow engine which can handle sequence flow.
For implementing of this workflow engine we used spring framework, but idea how to implement this should be same with any framework, if you use one, or  without framework.
We will start with short introduction to sequence workflow pattern, after that we will take look at needed interfaces and at end we will start with implementing workflow engine with spring.


Sequence workflow pattern

Sequence workflow pattern describe workflows in which every step (action) is done step by step one after another. On next image you can see how that should looks like:
Every action which will be processed inside flow share same context, which allow flow's participants to share information between each other. Idea of common context is used because every step should be independent one from each other and they should be easily added as part of some other flow.
If you want to get more information about sequence workflow pattern you can visit: Sequence pattern.

Defining needed interface

Next step is creating a set of interfaces which allow us to easy create workflow and define a workflow actions.
We can start with Workflow interface. This interface is responsible for processing workflow action, and actually it define what our workflow engine should to do. It is really simple interface with only one method "processWorkflow". 
This method is called by workflow engine and it used to supply workflow with initial objects which can be used inside of workflow, and it represent starting point of each workflow. 


package ba.codecentric.workflow;

import java.util.Map;

/**
* Process email workflow.
*
* @author igor.madjeric
*
*/
public interface Workflow {

  /**
  * Method for processing workflow.
  * 
  * @param parameters
  * maps of object which are needed for workflow processing
  * @return true in case that workflow is done without errors otherwise false
  */
  public boolean processWorkflow(Map<String, Object> parameters);

}


Next what we need is interface used for defining workflow action. This is also simple interface whit only one method too.



package ba.codecentric.workflow;


/**
* Define workflow action

* @author igor.madjeric

*/
public interface WorkflowAction {
  /**
   * Execute action.
   * 
   * @param context
   * @throws Exception
  */
  public void doAction(Context context) throws Exception;
}



So this interface define only doAction method which will be called by workflow implementation.

Last interface which we need to define is Context interface. This interface define two methods, one for setting object in context and another for retrieving it.

package ba.codecentric.workflow;

/**
 * Context interface.
 * 
 * Class which extend this interface should be able to provide mechanism for keeping object in context.<br />
 * So they can be shared between action inside workflow.
 * 
 * @author igor.madjeric
 *
 */
public interface Context {
/**
* Set value with specified name in context.
* If value already exist it should overwrite value with new one.
* @param name of attribute
* @param value which should be stored for specified name
*/
public void setAttribute(String name, Object value);
/**
* Retrieve object with specified name from context, 
         * if object does not exists in context it will return null. 
* @param name of attribute which need to be returned
* @return Object from context or null if there is no value assigned to specified name
*/
public Object getAttribute(String name);

}


This is all interfaces which we need to define for the our simple workflow

Implementing simple workflow engine


After we defined interfaces we can start with implementation of workflow engine. There is a some of requirements what engine should be able to do.
This engine should support sequence workflow what mean that action are executed one after another. 
Also the engine should be able to precess more then one flow.
Workflow action should be able to share information between each other.

As we see there is no lot of requirements so we should start with implementing it. 
First of all we can create context class which will be used for handling information between actions. This class implement Context interface, and don't do much other stuff.


package ba.codecentric.workflow.impl;

import java.util.HashMap;
import java.util.Map;
import ba.codecentric.workflow.Context;
/**
* Save states between different workflow action.
*
* @author igor.madjeric
*
*/
public class StandardContext implements Context {

   private Map<String, Object> context;

   /**
   * Create context object based.
   *
   * @param parameters
   */
   public StandardContext(Map<String, Object> parameters) {
      if (parameters == null) {
         this.context = new HashMap<String, Object>();
      } else {
         this.context = parameters;
      }
   }

   @Override
   public Object getAttribute(String name) {
      return context.get(name);
   }
  @Override
  public void setAttribute(String name, Object value) {
     context.put(name, value);
  }
}

Second step is creating class which implementing Workflow interface. We called this class StandardWorkflow. Beside implementing Workflow interface this class also implement ApplicationContextAware interface because of need for accessing to spring bean repository. If you don't use spring you don't need to implement it. 
We already said that workflow should support more then one flow. So action of one workflow can be defined as a list, and every of this list should be assigned to some logical name. So for the registration of actions we can use something like Map<String, List<WorkflowAction>>. First we will see spring bean definition of StandardWorkflow and of one custom flow and after that we will see implementation of StandardWorkflow.

Bean definition of StandardWorkflow:


<bean id="standardWorkflow"
class="de.codecentric.oev.external.services.workflow.standard.StandardWorkflow">
<property name="workflowActions">
<map>
<!-- <entry key="<CID>_action"><ref bean="<CID>_action"/></entry>-->
<!-- OEVBS -->
<entry key="action1_action">
<ref bean="action1_action" />
</entry>
<!-- PVN -->
<entry key="action2_action">
<ref bean="action2_action" />
</entry>
<!-- WPV -->
<entry key="action3_action">
<ref bean="action3_action" />
</entry>
</map>
</property>
</bean>

From this bean definition we can see that we define action per customer and the list of action are defined in referenced beans.
Here is an example of one of that customer beans:

<bean id="action1_action" class="java.util.ArrayList">
<constructor-arg>
<!-- List of Actions -->
<list value-type="ba.codecentric.workflow.WorkflowAction" >
<ref local="createEmailAction"/>
<ref bean="sendEmailAction"/>
</list>
</constructor-arg>
</bean>


Now we can see how StandardWorkflow look likes:



package ba.codecentric.workflow.impl;


import java.util.List;
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 ba.codecentric.workflow.Context;
import ba.codecentric.workflow.Workflow;
import ba.codecentric.workflow.WorkflowAction;


/**
 * Define standard workflow for sending email.
 * 
 * @see Workflow
 * 
 * @author igor.madjeric
 * 
 */
public class StandardWorkflow implements Workflow,
ApplicationContextAware {


private final Log LOG = LogFactory.getLog(StandardWorkflow.class);


private static final String ACTION = "action";


private Map<String, List<WorkflowAction>> workflowActions;


private ApplicationContext applicationContext;


/**
*@see de.codecentric.oev.external.services.workflow.Workflow#processWorkflow(java.util.Map)
*/
@Override
public boolean processWorkflow(String workflofName, Map<String, Object> parameters) {
Context context = new StandardContext(parameters);
List<WorkflowAction> actions = getWorkflowActions(workflofName);
for (WorkflowAction action : actions) {
try {
action.doAction(context);
} catch (Exception e) {
StringBuilder message = new StringBuilder("Failed to complete action:" + action.toString());
message.append("\n");
message.append(e.getMessage());
LOG.error(message.toString());
return false;
}
}
return true;
}
private List<WorkflowAction> getWorkflowActions(String actionName) {
List<WorkflowAction> actions = workflowActions.get(actionName);
if (actions == null || actions.isEmpty()) {
LOG.error("There is no defined action for " + actionName);
throw new IllegalArgumentException("There is no defined action for " + actionName);
}
return actions;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException{
  this.applicationContext = applicationContext;
}
// Getter/Setter
public Map<String, List<WorkflowAction>> getWorkflowActions() {
return workflowActions;
}
public void setWorkflowActions(
Map<String, List<WorkflowAction>> workflowActions) {
this.workflowActions = workflowActions;
}
}


Again as you can see this also is a simple class all work is done in processWorkflow method, to which we provide flow name and input parameters. This method create Context with a specified parameter, after that it try to load actions defined for specified flow, and if there is flow with specified name it start running flow.


How to start the flow


This depend of yours need. You can use a rest controller, or use any other mechanism like MBeans, scheduled jobs or you can make direct call from some of your services. All what you need to do is to call processWorkflow method.

Comments

Popular posts from this blog

Checking file's "magic numbers"

Few days ago I had very interesting task. Our customer required that we perform checking of so called file's "magic numbers" to determinate does uploaded file correspond to it's extension.  We are already allowed only to upload files with some predefined extensions (PDF, DOC ...). But this can not prevent some evil user to update an exe file after renaming it to PDF or DOC. So first of all I will explain what are "magic numbers", and then I will show how we handle them.

Running Spring Boot Web App on the Random Port from Port Range

By default the spring boot web application is listening on the port 8080 for the incoming connection. This behavior can be changed by providing server.port property value during starting of the application or as part of the application.properties or through the code by implementing EmbeddedServletContainerCustomizer. But it would be even better if we could specified a range of the ports which can be used for the starting the application. It would be great if I could specify a property like server.portRange=8100..8200 to define a list of the port on which I want to start my service. In this blog post I will describe how this can be done.