文章来源: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在保持面向对象的约束同时,减少面向对象操作麻烦的程度。