使用Spring来建立一个简单的工做流引擎

  原文地址:    [url]http://www.javaworld.com/javaworld/jw-04-2005/jw-0411-spring.html[/url]
  中文地址:    [url]http://www.matrix.org.cn/resource/article/43/43785_Spring.html[/url]

   摘要
  spring是支持控制反转编程机制的一个相对新的框架。本文把spring做为简单 工做 流引擎,将它用在了更加通用的地方。在对工做流简单介绍以后,将要介绍在基本工做流场景中基于Spring的工做流API的 使用

  许多 J2EE应用程序要求在一个和主机分离的上下文中执行处理过程。在许多状况下,这些后台的进程执行多个任务,一些任务依赖于之前任务的状态。因为这些处理任务之间存在相互依赖的关系,使用一套基于过程的方法调用经常不能知足要求。 开发 人员可以利用Spring来容易地将后台进程分离成活动的集合。Spring容器链接这些活动,并将它们组织成简单的工做流。

  在本文中,简单工做流被定义成不须要用户干预,以必定顺序执行的任意活动的集合。然而,咱们并不建议将这种方式代替存在的工做流框架。在一些场景中,须要更多的用户交互,例如基于用户输入而进行的转向,链接或传输,这时,比较好的方法是配用一个单独的开源或者商业的工做流引擎。一个开源 项目 已经成功地将更复杂的工做流设计集成到spring中。

  若是你手上的工做流任务是简单的,那么,与功能完备的独立工做流框架相比,简单工做流的策略就会变得有意义,特别地,若是已经使用了spring,这种快速实现能够保证 时间 不会变得更加漫长。此外,考虑到spring轻量级的控制反转容器的特色,spring在资源负载上减小了资源负载。

  这篇文章简短地从编程主题的角度介绍工做流。经过使用工做流的概念,spring被用来做为 驱动工做流 引擎 的框架。而后,讨论了生产部署选项。如今,让咱们从工做流的设计模式和相关背景信息来介绍简单工做流的思想吧。
 

简单工做流
  工做流模型是一个早在70年代就有人开始研究的主题,许多开发者都试图建立工做流模型规范。W.H.M. van der Aalst等人写了《工做流模型》白皮书(2003年7月),它成功地提炼出一组设计模式,这些设计模式准确地将大多数通用的工做流场景建模。当中,最普通的工做流模式是顺序模式 (Sequence pattern)。顺序工做流模式知足了简单工做流的设计原则,而且由一组顺序执行的活动组成。

   UML(统一建模语言)活动图一般被用来做为一个机制对工做流建模。图1显示了一个基本的使用标准UML活动图对顺序工做流过程的建模过程。
图 1顺序 工做 流模式
  顺序工做流是一个在 J2EE中流行的标准工做流模式。J2EE应用程序在后台线程中,一般须要一些顺序发生的事件或者异步事件。图2中的活动图描述了一个简单的工做流,用来通知感兴趣的旅行者,他们感兴趣的目的地的机票 价格 已经降低的事件。
图 2.机票价格降低的简单工做流
  图1中的航线工做流负责建立和发送动态的email通知。过程当中的每一步表示了一个活动(activity)。在工做流处于活动以前,一些额外事件必须发生。在这个例子中,事件是飞行路线费率的减小。

  让咱们来简要的看一下航线工做流的业务逻辑。若是第一个活动找不到对费率减小通知感兴趣的用户,那么整个工做流就被取消。若是发现了感兴趣的用户,那么接下来的活动继续执行。随后,一个XSL(扩展样式表)转换生成消息内容,以后,记录审计信息 (audit information)。最后,工做流试图经过SMTP 服务器发送这个消息。若是这个任务没有错误地完成,便在日志中记录 成功 的信息,进程结束。可是,若是在和SMTP服务器通信时发生了错误,一个特别的错误处理例程将要 管理 这些错误。错误处理代码将会试着去从新发送消息。

  考虑这个航线的例子,一个明显的问题是:你怎么样有效地将顺序处理过程分解为单独的活动?这个问题被spring巧妙的处理了。下面,让咱们快速地讨论spring的反转控制框架。
 
控制反转
  Spring经过使用spring容器来负责控制对象之间的依赖关系,使得咱们再也不对对象之间的依赖负责。 这种依赖关系的实现就是你们所知道的控制反转(IoC)或依赖注射。参见Martin Fowler's "Inversion of Control Containers and the Dependency Injection Pattern"(martinfowler.com, 2004年2月)获得关于控制反转和依赖注射的更加深刻的讨论。经过管理对象之间的依赖关系,spring就不须要那些只是为了使类可以相互协做,而将对象粘合的代码。

做为spring beans的工做流组件
  在进一步讨论以前,如今是简要介绍spring中主要概念的恰当时候。接口ApplicationContext是从接口BeanFactory继承的,它被用来做为在spring容器内实际的控制实体和容器。

  ApplicationContext负责对一组做为spring beans的一组bean的初始化,配置和生命期管理。咱们经过装配在一个基于 XML的配置文件中的spring beans来配置ApplicationContext。这个配置文件说明了spring beans互相协做的本质特色。这样,用spring的术语来讲,与其余spring beans交互的spring beans就被叫着协做者(collaborators)。缺省状况下,spring beans是做为单例存在于ApplicationContext中的,可是,单例的属性可以被设置为false,从而有效地改变他们在spring中调用原型模式时的行为。

  回到咱们的例子,在飞机票价降低的时候,一个SMTP发送例程的抽象就被装配在工做流过程例子中的最后的活动(例子代码能够在 Resources中获得)。因为是第5个活动,咱们命名它为activity5。要发送消息,activity5就要求一个代理协做者和一个错位处理句柄。
<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>

  将工做流组件实施成spring beans产生了两个使人喜悦的结果,就是容易进行单元测试和很大程度上可重用能力。IoC容器的特色明显地提供了有效的单元测试。使用像spring这样的Ioc容器,在测试期间,协做者之间的依赖可以容易的用假的替代者替代。在这个航线的例子中,可以容易地从惟一的测试ApplicationContext中检索出像activity5活动这样的spring bean。用一个假的SMTP代理SMTP 服务器,就有可能单独地测试activity5。

  第二个意外的结果,可重用能力是经过像XSL转换这样的工做流活动实现的。一个被抽象成工做流活动的XSL转换如今可以被任何处理XSL转换的工做流所重用。
 
装配工做流
  在提供的API中(从Resources 下载),spring控制了一些操做者以一种工做流的方式交互。关键接口以下:

  Activity: 封装了工做流中一个单步业务逻辑

  ProcessContext:在工做流活动之间传递具备ProcessContext类型的对象。实现了这个接口的对象负责维护对象在工做流转换中从一个活动转换到另外一个活动的状态。

  ErrorHandler: 提供错误处理的回调方法。

  Processor: 描述一个做为主工做流线程的执行者的bean。

  下面从例子 源码中摘录的代码是将航线例子装配为简单工做流过程的spring bean的配置。
<!-- 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>

  SequenceProcessor类是一个对顺序模式建模的具体子类。有5个活动被链接到工做流处理器,工做流处理器将顺序执行这5个活动。

  与大多数过程式后台进程相比,工做流的 解决方案真正的突出了高度强壮的错误处理。错误处理句柄能够单独地处理每一个活动。这种类型的句柄在单一活动级别提供了细致的错误处理。若是没有单独处理单个活动的错误处理句柄,那么全局工做流处理器的错误处理句柄将会处理出现的问题。例如,若是在工做流处理过程当中的任意时刻,一个没有被处理的错误出现了,那么它将会向外传播,被使用defaultErrorHandler属性装配的ErrorHandler Bean处理。

  更复杂的工做流框架将工做流转换之间的状态持久化存储到 数据库中。在这篇文章中,咱们仅仅对状态转换是自动完成的工做流感兴趣。状态信息仅仅在实际工做流运行时在ProcessContext中获得。在ProcessContext中,你仅仅能看到ProcessContext的接口的两个方法:
public interface ProcessContext extends Serializable
{
public boolean stopProcess();
public void setSeedData(Object seedObject);
}

  用于航线例子工做流的具体的ProcessContext类是RateDropContext类。RateDropContext类封装了用于执行航线费率下降工做流必须的数据。

  到如今为止,经由缺省的ApplicationContext的做用,全部bean实例都已经成了单例。可是,对于每个航线工做流的调用,咱们必须建立一个新的RateDropContext类的实例。为了处理这种需求,须要配置SequenceProcessor,采用全限定类名做为processContextClass属性的值。对于每一个工做流的执行,SequenceProcessor使用指定的类名从spring检索ProcessorContext类的一个新的实例。为了使这种机制可以起做用,非单件的spring bean或者类型org.iocworkflow.test.sequence.simple.SimpleContext的原型必须存在于ApplicationContext中。
 
播种工做流
  既然咱们知道怎样使用spring来组装一个简单的工做流,就让咱们集中精力使用种子数据(seed data)示例工做流的过程。要明白怎样开始工做流,看一看在实际接口Processor上表现的方法:
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);
}

  大多数状况下,工做流须要一些初始化激活才能开始。开始一个处理过程有两个选项:doActivities(ObjectseedData)方法或者无参数的doActivities()。下面的代码列表是包含在样例代码中为SequenceProcessor而实现的doActivities():
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;
}
}

  在这个航空费用减小的例子中,工做流过程的种子数据包括航线信息和费率减小的信息。使用容易测试的航线工做流例子,经过doActivities(Object seedData)方法发出种子数据并激活一个单一的工做流过程是简单的:
BaseProcessor processor = (BaseProcessor)context.getBean("rateDropProcessor");
processor.doActivities(createSeedData());

  这些代码是从包含在这篇文章中的测试例子中摘录的。rateDropProcessor Bean是从ApplicationContext中检索来的。rateDropProcessor其实是装配成SequenceProcessor的实例来处理顺序执行。createSeedData()方法实例化一个对象,这个对象封装了初始化航线工做流所须要的全部种子数据。
 
Processor选项
  虽然包含在源代码中的Processor具体的子类仅仅是SequenceProcessor,可是,许多Processor接口的实现也是能够想象获得的。能够开发其余工做流处理过程子类来控制不一样的工做流类型,例如,另外一种像并行切割模式那样有着变化的执行路径的工做流。对于简单工做流来讲,由于活动的顺序是预先决定了的,因此SequenceProcessor是好的选择。尽管没有被包括进来,对于使用基于spring的简单工做流的实现来讲,排他选择模式是另外一个好的选择。当使用排他选择模式时,在每一个活动执行以后,Processor具体类就会讯问ProcessorContext,接下来将要执行哪个活动。

  注:有关并行切割,排他选择和其余工做流模式的更多信息,请参看W.M.P. van der Aalst等人写的《工做流模式》一书。

启动工做流
  考虑到工做流过程经常须要异步执行的特色,使用分离的执行线程来启动工做流就变得有意义了。对于工做流的异步启动而言,有好几个选项;咱们主要集中在其中的两个:积极地检测(actively polling)一个队列来启动工做流,或者使用经过ESB(ent ERPrise service bus, 企业服务总线)的事件 驱动方式来启动工做流,而Mule就是ESB的一个开源项目。

  图3和图4描绘了两种启动策略。图3中,积极检测在工做流中第一个活动常常检查资源的情形下发生,好比数据源或POP3邮件账户。若是图3中的积极检测发现有任务等待处理,那么启动就会开始。
图 3. 经过积极检测来启动工做流
  另外一方面,图4表示了使用JMS( Java消息服务)的 J2EE应用程序把事件放到队列上的情形。一个经过ESB配置的事件监听器收到图4中的事件,而且开始工做流,这样,启动工做流过程。
图 4. 经过ESB事件来启动工做流
  使用所提供样例的代码,让咱们更详细的看看主动选择启动方式与事件驱动的启动方式。
 
积极检测
  积极检测是一种花费较少的启动工做流过程的方案。SequenceProcessor足够灵活,以使得可以经过平滑的选择工做来进行启动过程。尽管并不使人满意,在没有时间进行事件 驱动子系统的配置和部署的许多情景中,积极检测是明智的选择。

  使用Spring的ScheduledTimerTask,检测模式就可以容易地装配。缺点就是必须建立额外的活动来进行检测。这个检测活动必须被设计来讯问某些实体,如 数据库表,pop邮件账户,或者 Web服务,而后决定新的工做是否等待参与到工做流中。

  在所提供的例子中,PollingTestCase类实例化一个基于检测的工做流过程。使用一个有着积极检测处理过程与事件驱动的启动过程的不一样之处在于,spring支持doActivities()方法的无参数版本。相反地,在事件驱动的启动中,启动处理过程的实体经过doActivities(Object seedData)方法提供了种子数据来启动工做流。检测方法的另外一个缺点是:资源不必定可以被重复地使用。依赖于应用程序环境,这种资源的消耗是不可接受的。

  下面代码例子演示了使用积极检测来控制工做流启动的一个活动:
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;
}
}

  此外,包含在例子代码的单元测试中的PollRates类提供了一个主动选举启动的能够运行的例子。PollRates模拟了对于航线费率降低的重复检查。

经过ESB的事件驱动启动工做流
  理想地,一个包含了适当的种子数据的线程可以异步地启动工做流。这种状况的一个例子是收到从 Java消息服务队列的消息。一个监听JMS队列或者主题的客户会收到通知,这个通知告知处理应该在onMessage()方法中开始工做流。而后,经过使用Spring和doActivities(Object seedData)方法就可以得到工做流处理器Bean。

  使用ESB,实际用于发送启动事件的机制可以恰当地从工做流处理器中分离出来。开源项目Mule ESB有紧凑地和Spring相集成的好处。任意传送机制,好比JMS,JVM,或者POP3邮箱都可以发起事件的传播。

工做流的连续运行
  工做流引擎后台进程应该可以没有干扰地连续运行。对于正在运行的基于spring的工做流单一进程来讲好,有几个选项。一个有着main()方法的简单Java类就足够演示与这篇文章伴随着的单元测试中的例子了。一个更加可靠的用于部署的机制是嵌入工做流到某种形式的 J2EE组件中。Spring很好地支持和J2EE兼容的web应用程序归档或者war文件的集成。基于Java管理附件(JMX)服务归档和JBoss应用 服务器(更多信息,参见JBoss homepage)支持的sar文件是更加合适的可部署组件,这种更合适的可部署组件也可以被用来将部署归档。在JBoss 4.0中,sar文件已经被你们所知道的deployer的格式所取代了。

例子代码
  打包成zip格式的例程代码最好是用Apache Maven来使用它们。你可以在主源代码目录src/java找到API。src/java目录中有三个单元测试,包括:SimpleSequenceTestCase,RateDropTestCase和PoolingTestCase。要运行全部这些测试,在命令行shell中键入maven test,而后在编译和运行以前,Maven将会 下载全部必需的jar文件。实际的XSL转换将会发生在两个测试中,它们的结果被管道输出到控制台。键入maven test:ui来拉出图形化的测试运行器,而后选择你想要运行的测试,而且观察控制台的结果。

结论   在这篇文章中你已经经过设计模式看到了工做流过程种类,在这些模式中,咱们主要集中介绍了顺序模式。经过使用接口,咱们来对基本工做流组件建模。经过装配多个接口实现到Spring,实现一个顺序工做流。最后还讨论了启动和部署工做流的不一样选项。   这里所提出的简单工做流技术确定不是最终的和革命性的。可是,使用Spring来实现像工做流这样的通用任务是一个经过使用IoC容器而得到的效率的好的示例。因为减小了粘合性代码的须要,Spring在保持面向对象的约束同时,减小面向对象操做麻烦的程度。
相关文章
相关标签/搜索