文章来源:matrix 作者:Steve Dodge
摘要
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(enterprise 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在坚持面向对象的约束同时,减少面向对象操作费事的水平。 |