Use Spring to create a simple workflow engine

Organize your backend processing tasks into an easy-to-use Spring-based workflow

Many J2EE applications require processing to be executed in a context separate from that of the main system. In many cases, these backend processes perform several tasks, with some tasks dependent upon a previous task's status. With the requirement of interdependent processing tasks, an implementation using a single procedural-style set of method calls usually proves inadequate. Utilizing Spring, a developer can easily separate a backend process into an aggregation of activities. The Spring container joins those activities to form a simple workflow.

For this article's purposes, simple workflow is defined as any set of activities performed in a predetermined order without user interaction. This approach, however, is not suggested as a replacement for existing workflow frameworks. For scenarios where more advanced interactions are necessary, such as forking, joining, or transitions based on user input, a standalone open source or commercial workflow engine is better equipped. One open source project has successfully integrated a more complex workflow design with Spring (see OSWorkflow).

If the workflow tasks at hand are simplistic, the simple workflow approach makes sense as opposed to a fully functional standalone workflow framework, especially if Spring is already in use, as quick implementation is guaranteed without incurring ramp-up time. Additionally, given the nature of Spring's lightweight Inversion-of-Control container, Spring cuts down on resource overhead.

This article briefly introduces workflow as a programming topic. Using workflow concepts, Spring is employed as the framework for driving a workflow engine. Then, production deployment options are discussed. Let's begin with the idea of simple workflow by focusing on workflow design patterns and related background information.

Simple workflow

Modeling workflow is a topic that has been studied as far back as the 1970s, and many developers have attempted to create a standardized workflow modeling specification. Workflow Patterns, a white paper by W.H.M. van der Aalst et al. (July 2003), has succeeded in classifying a set of design patterns that accurately model the most common workflow scenarios. Among the most trivial of the workflow patterns is the Sequence pattern. Fitting the criteria of a simple workflow, the Sequence workflow pattern consists of a set of activities executed in sequence.

UML (Unified Modeling Language) activity diagrams are commonly used as a mechanism to model workflow. Figure 1 shows a basic Sequence workflow process modeled using a standard UML activity diagram.

Figure 1. Sequence workflow pattern

The Sequence workflow is a standard workflow pattern prevalent in J2EE applications. A J2EE application usually requires a sequence of events to occur in a background thread or asynchronously. Figure 2's activity diagram illustrates a simple workflow for notifying interested travelers that the airfare to their favorite destination has decreased.

Figure 2. Simple workflow for airfare decrease. Click on thumbnail for full-sized image.

The airline workflow in Figure 1 is responsible for creating and sending dynamic email notifications. Each step in the process represents an activity. Some external event must occur before the workflow is set in motion. In this case, that event is a rate decrease for an airline's flight route.

Let's walk through the airline workflow's business logic. If the first activity finds no users interested in rate-drop notifications, the entire workflow is canceled. If interested users are discovered, the remaining activities are completed. Subsequently, an XSL (Extensible Stylesheet Language) transformation generates the message content, after which audit information is recorded. Finally, an attempt to send the message through an SMTP server is made. If the submission completes without error, success is logged and the process terminates. But, if an error occurs while communicating with the SMTP server, a special error-handling routine will take over. This error-handling code will attempt to resend the message.

Given the airline example, one question is evident: How could you efficiently break up a sequential process into individual activities? This problem is handled eloquently using Spring. Let's quickly discuss Spring as an Inversion of Control framework.

Inverting control

Spring allows us to remove the responsibility of controlling an object's dependencies by moving this responsibility to the Spring container. This transfer of responsibility is known as Inversion of Control (IoC) or Dependency Injection. See Martin Fowler's "Inversion of Control Containers and the Dependency Injection Pattern" (martinfowler.com, January 2004) for a more in-depth discussion on IoC and Dependency Injection. By managing dependencies between objects, Spring eliminates the need for glue code, code written for the sole purpose of making classes collaborate with each other.

Workflow components as Spring beans

Before we get too far, now is a good time to walk through the main concepts behind Spring. The ApplicationContext interface, inheriting from the BeanFactory interface, imposes itself as the actual controlling entity or container within Spring. The ApplicationContext is responsible for instantiation, configuration, and lifecycle management of a set of beans known as Spring beans. The ApplicationContext is configured by wiring up Spring beans in an XML-based configuration file. This configuration file dictates the nature in which Spring beans collaborate with each other. Thus, in Spring speak, Spring beans that interact with others are known as collaborators. By default, Spring beans exist as singletons in the ApplicationContext, but the singleton attribute can be set to false, effectively changing them to behave in what Spring calls prototype mode.

Back to our example, in the airfare decrease, an abstraction of an SMTP send routine is wired as the last activity in the workflow process example (example code available in Resources). Being the fifth activity, this bean is aptly named activity5. To send a message, activity5 requires a delegate collaborator and an error handler:

 <bean id="activity5" 
      class="org.iocworkflow.test.sequence.ratedrop.SendMessage">
      <property name="delegate">
         <ref bean="smtpSenderDelegate"></ref>
      </property>
      <property name="errorHandler">
         <ref bean="mailErrorHandler"/>
      </property>
   </bean>

Implementing the workflow components as Spring beans results in two desirable by-products, ease of unit testing and a great degree of reusability. Efficient unit testing is evident given the nature of IoC containers. Using an IoC container like Spring, collaborator dependencies can easily be swapped with mock replacements during testing. In the airline example, an Activity Spring bean such as activity5 can easily be retrieved from a standalone test ApplicationContext. Substituting a mock SMTP delegate into activity5 makes it possible to unit test activity5 separately.

The second by-product, reusability, is realized by workflow activities such as an XSL transformation. An XSL transformation, abstracted into a workflow activity, can now be reused by any workflow dealing with XSL transformations.

Wiring up the workflow

In the provided API (downloadable from Resources), Spring controls a small set of players to interact in a manner that constitutes a workflow. The key interfaces are:

  • Activity: Encapsulates business logic of a single step in the workflow process.
  • ProcessContext: Objects of type ProcessContext are passed between activities in the workflow. Objects implementing this interface are responsible for maintaining state as the workflow transitions from one activity to the next.
  • ErrorHandler: Provides a callback method for handling errors.
  • Processor: Describes a bean serving as the executer of the main workflow thread.

The following excerpt from the sample code is a Spring bean configuration that binds the airline example as a simple workflow process.

 <!-- Airline rate drop as a simple sequence workflow process -->
   <bean id="rateDropProcessor" class="org.iocworkflow.SequenceProcessor" >
      <property name="activities">
         <list>
            <ref bean="activity1"/><!--Build recipients-->
            <ref bean="activity2"/><!--Construct DOM tree-->
            <ref bean="activity3"/><!--Apply XSL Transform-->
            <ref bean="activity4"/><!--Write Audit Data-->
            <ref bean="activity5"/><!--Attempt to send message-->
         </list>
      </property>
      <property name="defaultErrorHandler">
         <ref bean="defaultErrorHandler"></ref>
      /property>
      <property name="processContextClass">
         <value>org.iocworkflow.test.sequence.ratedrop.RateDropContext</value>
      </property>
   </bean>

The SequenceProcessor class is a concrete subclass that models a Sequence pattern. Wired to the processor are five activities that the workflow processor will execute in order.

When compared with most procedural backend process, the workflow solution really stands out as being capable of highly robust error handling. An error handler may be separately wired for each activity. This type of handler provides fine-grained error handling at the individual activity level. If no error handler is wired for an activity, the error handler defined for the overall workflow processor will handle the problem. For this example, if an unhandled error occurs any time during the workflow process, it will propagate out to be handled by the ErrorHandler bean, which is wired up using the defaultErrorHandler property.

More complex workflow frameworks persist state to a datastore between transitions. In this article, we're only interested in simple workflow cases where state transition is automatic. State information is only available in the ProcessContext during the actual workflow's runtime. Having only two methods, you can see the ProcessContext interface is on a diet:

 public interface ProcessContext extends Serializable {
      public boolean stopProcess();    
      public void setSeedData(Object seedObject);   }

The concrete ProcessContext class used for the airline example workflow is the RateDropContext class. The RateDropContext class encapsulates the data necessary to execute an airline rate drop workflow.

Until now, all bean instances have been singletons as per the default ApplicationContext's behavior. But we must create a new instance of the RateDropContext class for every invocation of the airline workflow. To handle this requirement, the SequenceProcessor is configured, taking a fully qualified class name as the processContextClass property. For every workflow execution, the SequenceProcessor retrieves a new instance of ProcessContext from Spring using the class name specified. For this to work, a nonsingleton Spring bean or prototype of type org.iocworkflow.test.sequence.simple.SimpleContext must exist in the ApplicationContext (see rateDrop.xml for the entire listing).

Seeding the workflow

Now that we know how to piece together a simple workflow using Spring, let's focus on instantiation using seed data. To understand how to seed the workflow, let's look at methods exposed on the actual Processor interface:

 public interface Processor {
      public boolean supports(Activity activity);
      public void doActivities();
      public void doActivities(Object seedData);
      public void setActivities(List activities);
      public void setDefaultErrorHandler(ErrorHandler defaultErrorHandler);
   }

In most cases, workflow processes require some initial stimuli for kickoff. Two options exist for kicking off a processor: the doActivities(Object seedData) method or its no-argument alternative. The following code listing is the doAvtivities() implementation for the SequenceProcessor included with the sample code:

 

public void doActivities(Object seedData) {

//Retrieve injected by Spring List activities = getActivities();

//Retrieve a new instance of the Workflow ProcessContext ProcessContext context = createContext();

if (seedData != null) context.setSeedData(seedData);

//Execute each activity in sequential order for (Iterator it = activities.iterator(); it.hasNext();) {

Activity activity = (Activity) it.next();

try { context = activity.execute(context);

} catch (Throwable th) { //Determine if an error handler is available at the activity level ErrorHandler errorHandler = activity.getErrorHandler(); if (errorHandler == null) { getDefaultErrorHandler().handleError(context, th); break; } else { //Handle error using default handler errorHandler.handleError(context, th); } } //Ensure it's ok to continue the process if (processShouldStop(context, activity)) break; } }

In the example of an airfare reduction, the workflow process seed data includes airline route information and rate decrease. With the easy-to-test airline workflow example, seeding and kicking off a single workflow process via the doActivities(Object seedData) method is simple:

 BaseProcessor processor = (BaseProcessor)context.getBean("rateDropProcessor");
   processor.doActivities(createSeedData());

This excerpt is from the example test case included with this article. The rateDropProcessor bean is retrieved from the ApplicationContext. The rateDropProcessor is actually wired as an instance of SequenceProcessor to handle sequential execution. The createSeedData() method instantiates an object encapsulating all the seed data needed for the airline workflow to be initiated.

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more