Use Spring to create a simple workflow engine

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.

 

Processor options

Although the only concrete subclass of Processor included with the source code is the SequenceProcessor, many implementations of the Processor interface are conceivable. Other workflow processor subclasses could be developed to control different workflow types—for example, other workflows with varying execution paths like the Parallel Splits pattern. The SequenceProcessor is a good candidate for simple workflows because the activity order is predetermined. Although not included here, the Exclusive Choice pattern is another good candidate for implementation using a Spring-based simple workflow. Regarding Exclusive Choice, after execution of each Activity, the Processor concrete class asks the ProcessContext which Activity to execute next.

Note: For more information on the Parallel Splits, Exclusive Choice, and other workflow patterns, please see Workflow Patterns, W.M.P. van der Aalst et al.

Kicking off the workflow

Given the asynchronous nature at which a workflow process is often required to perform, it makes sense that a separate thread of execution must exist to kick off the workflow. Several options exist for kicking off the workflow asynchronously; we focus on two, actively polling a queue or using an event-driven kickoff through an ESB (enterprise service bus) such as Mule, an open source ESB (for more information on Mule, see "Event-Driven Services in SOA" (JavaWorld, January 2005)).

Figures 3 and 4 depict the two kickoff strategies. In Figure 3, active polling takes place where the first activity in a workflow is constantly checking a resource such as a datasource or a POP3 email account. If the polling activity in Figure 3 finds a task waiting to be processed, kickoff begins.

On the other hand, Figure 4 is a representation of a standard J2EE application using JMS (Java Message Service) to place an event on a queue. An event listener configured through the ESB picks up the event in Figure 4 and seeds the workflow, thus kicking off the process.

Figure 3. Kickoff via active polling. Click on thumbnail to view full-sized image.

Figure 4. Event-driven kickoff through ESB. Click on thumbnail to view full-sized image.

Using the sample code provided, let's examine in more detail the active polling kickoff versus the event-driven kickoff.

Active polling

Actively polling is the poor man's solution to kicking off a workflow process. The SequenceProcessor is flexible enough to make kickoff via polling work smoothly. Although not desirable, active polling shakes out as the logical choice in many situations where the time is not available for configuration and deployment of an event-driven subsystem.

Using Spring's ScheduledTimerTask, a polling scheme can be easily wired up. One drawback is an additional Activity that must be created to do the polling. This polling Activity must be designed to interrogate some entity—e.g., a database table, a pop mail account, or a Web service—and determine if new work is waiting to be attended to.

In the examples provided, the PollingTestCase class instantiates a polling-based workflow processor. Using a processor with active polling differs from an event-driven kickoff in that Spring favors the no-argument version of the doActivities() method. Conversely, in the case of an event-driven kickoff, the entity kicking off the processor provides seed data through the doActivities(Object seedData) method. Another polling drawback: resources are unnecessarily exercised repeatedly. Depending on the application environment, this resource drain may not be acceptable.

The following code example demonstrates an activity that uses active polling to control the workflow kickoff:

 public class PollForWork implements Activity
{
   public ProcessContext execute(ProcessContext context) throws Exception {

//First check if work needs to be done boolean workIsReady = lookIntoDatabaseForWork();

if (workIsReady) { //The Polling Action must also load any seed data ((MyContext) context).setSeedData(createSeedData());



} else { //Nothing to do, terminate the workflow process for this iteration ((MyContext) context).setStopEntireProcess(true); } return context; } }


Additionally, the PollRates class, included with the unit tests in the sample code, provides a working example of active polling kickoff. PollRates simulates the repetitive check for falling airline rates.

Event-driven kickoff via ESB

Ideally, a thread containing the proper seed data would be available to kick off the workflow asynchronously. An example of this is a message received from a Java Message Service queue. A client listening on a JMS queue or topic would receive notification that processing should start in the onMessage() method. Then, the workflow processor bean would be acquired using Spring and the doActivities(Object seedData) method.

Using an ESB, the actual mechanism used to send a kickoff event can be eloquently decoupled from the workflow processor. The open source Mule ESB has the benefit of being tightly integrated with Spring. Any transport mechanism such as JMS, the JVM, or a POP3 mailbox could initiate event propagation.

Running continuously

Backend processes such as a workflow engine should be able to run continuously without intervention. Several options exist for running the Spring-based workflow standalone. A simple Java class with a main() method would suffice as demonstrated in the unit tests accompanying this article. A more reliable mechanism for deploying is to embed the workflow in some form of J2EE component. Spring has well supported integration with the J2EE-compliant webapp archive or war file. Deployment could also be achieved using a more proprietary deployable component such as the Java Management Extensions (JMX)-based service archive or sar file supported by the JBoss Application Server (for more information, see the JBoss homepage). As of JBoss 4.0, the sar file has been replaced by a format known as the deployer.

Example code

The example code, bundled in zip format, is best utilized with Apache Maven. The API can be found under the main source directory src/java. Three unit tests exist in the src/test directory, including SimpleSequenceTestCase, RateDropTestCase, and PoolingTestCase. To run all the tests, type maven test in a command shell and Maven will download all the necessary jar files before compiling and running. Actual XSL transformations will take place for two of the tests with results piped to the console. After successful execution of the test suite, you may wish to run the tests individually and watch output in real time. Try typing maven test:ui to pull up the graphical test runner, then select the test you want to run and watch the output in the console.

Summary

In this article, you have seen the categorization of workflow processes through design patterns, of which we focused on the Sequence pattern. Using interfaces, basic workflow components were modeled. By wiring up various interface implementations into Spring, a sequence workflow engine materialized. Finally, options for kickoff and deployment were discussed.

The simple workflow technique presented here is definitely not earth shattering or revolutionary. But, using Spring for general-purpose tasks like workflow is a good demonstration of the efficiencies gained by IoC containers. Eliminating the need for glue code, Spring makes adhering to object-oriented constraints less cumbersome.

I'd like to recognize Mikhail Garber, author of a nonrelated article in JavaWorld (see "Use Search Engine Technology for Object Persistence"). Mikhail was a key contributor, helping with the original idea of using Spring in a simple workflow capacity.

Author Bio

Steve Dodge specializes in building commercial software with J2EE-based open source frameworks. His development experience spans more than seven years and has worked on development projects for various governmental and commercial entities such as The United States Postal Service, The National Oceanic and Atmospheric Administration, EDS, and Verizon Wireless.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值