这一章解释了流程定义的基础,流程虚拟机给予的功能 以及活动实现是如何构建的。 同时,客户端API被用来执行包含了那些活动实现的流程。
PVM库没有包含完整的流程结构。
作为替代的是活动的运行时行为被委派给一个ActivityBehaviour。
换句话讲,ActivityBehaviour是一个接口,
它用来在纯java环境实现流程结构的运行时行为。
public interface ActivityBehaviour extends Serializable { void execute(ActivityExecution execution) throws Exception; }
当一个活动行为被调用时,它就处于执行传播的全部控制中。
换句话说,一个活动行为可以决定下一步应该做走哪个执行。
比如,可以使用execution.take(Transition)获得一个转移,
或者使用execution.waitForSignal()进入等待阶段。
万一活动行为没有调用任何上述的执行传播方法,
执行将
按默认方式执行。
我们会启动一个非常原始的hello world例子。 一个Display活动会将一条信息打印到控制台:
public class Display implements ActivityBehaviour { String message; public Display(String message) { this.message = message; } public void execute(ActivityExecution execution) { System.out.println(message); } }
让我们使用这个活动构建我们第一个流程定义:
TODO add ProcessBuilder example code
现在我们可以像下面这样执行流程:
Execution execution = processDefinition.startExecution();
startExecution的调用会在控制台打印hello world:
hello world
一个总是值得提醒的事情是活动可以使用属性进行配置。 在Display例子中,你可以看到message属性在两种使用方法中配置的不同。 通过配置属性,我们可以写出可复用的活动。 它们以后每次在流程中使用都可以进行不同的配置。 这是一个基本的部分, 将流程语言构建在流程虚拟机之上。
其他需要解释的部分是
这个活动实现没有包含任何执行传播的功能。
当一个新流程实例启动时,
执行会定位到初始活动,那个活动会被执行。
Display.execute方法用来决定默认的执行传播。
具体的,这意味着活动自己
没有调用任何执行传播的方法。
那种情况下,默认的传播会执行。默认传播会选择第一个转移,如果这个转移存在的话。
如果没有,它会结束这个执行。
这揭示了为什么a活动和b活动都被执行,
而在b活动执行完执行会停止。
关于默认流程行为的更多细节可以 在第 9.2 节 “默认执行行为”找到。
外部活动是负责流程执行由外部转移进来的活动, 外部的意思是来自流程系统的外部。 这意味着这个执行流程对于系统来说,这是一个等待状态。 这个执行会一直等待到外部触发器调用。
为了处理外部触发器,ExternalActivityBehaviour
为ActivityBehaviour添加了一个方法:
public interface ExternalActivity[1] extends Activity { void signal(Execution execution, String signal, Map<String, Object> parameters) throws Exception; }
就像普通的活动,当一个执行到达一个活动,
外部活动行为的execute方法会被调用。
在外部活动中,execute方法会传递另一个系统的响应,
然后通过调用execution.waitForSignal()
进入等待状态。
比如在execute方法中,响应可能是由一个人传入,
通过在任务管理系统中创建一个任务入口,
然后等待到这个人完成这个任务。
一旦活动行为已经处于等待状态,
然后执行会等待到调用signal方法。
执行会委派signal给ExternalActivityBehaviour对象
分配给当前的活动。
所以活动的signal方法
会在等待期间,在执行获得一个外部触发器的时候调用。
signal方法中,响应会传递给后面的流程执行。
比如,当一个人完成了一个任务,任务管理系统
会在执行中调用signal方法。
一个signal可选择使用signal名字和一个参数map。 活动行为拦截signal和参数的最常用方式是 signal对应选择的外出转移, 参数作为执行中的变量。但那些只是例子, 它一直等到活动使用singal和它期望的参数。
这里是一个简单等待状态实现的第一个例子:
public class WaitState implements ExternalActivity { public void execute(ActivityExecution execution) { execution.waitForSignal(); } public void signal(ActivityExecution execution, String signalName, Map<String, Object> parameters) { execution.take(signalName); } }
execute方法调用execution.waitForSignal()。
execution.waitForSignal()的调用
会使流程执行进入等待状态,
直到一个外部触发器出现。
signal方法使用signal参数对应的转移名称
来选择转移。所以当一个执行获得一个外部触发器,
signal名称被拦截,作为外部转移的名称,
执行会被传播到那个转移上。
这里是从a到b有一个转移的简单流程。 这时候,两个活动的行为都是WaitState。
ClientProcessDefinition processDefinition = ProcessFactory.build()
.activity("a").initial().behaviour(new WaitState())
.transition().to("b")
.activity("b").behaviour(new WaitState())
.done();
让我们为流程定义启动一个新流程实例:
ClientExecution execution = processDefinition.startProcessInstance();
启动这个流程会执行a中的WaitState活动。
WaitState.execute会调用
ActivityExecution.waitForSignal。
所以当processDefinition.startProcessInstance()返回,
执行会一直处在a活动。
assertEquals("a", execution.getActivityName());
然后我们提供了外部触发器,
通过调用signal方法。
execution.signal();
execution.signal()会委派给当前活动。
所以在这种情况下就是a活动里的
WaitState活动。WaitState.signal会调用
ActivityExecution.take(String transitionName)。
当我们没有提供一个signal名称,第一个名字是null会被选中。
我们指定的a的唯一转移没有名字,所以会选中这个。
然后这个转移指向b。
当执行到达b活动,
b活动中的WaitState活动会被执行。
就像我们上面看到的,执行会在b一直等待,
这时signal会返回,
离开的执行指向b活动。
assertEquals("b", execution.getActivityName());
在下一个例子里,我们会结合自动活动和等待状态。
这里例子构建了贷款审批流程,使用WaitState
和Display活动,我们刚刚创建的。
贷款流程的图形看起来像这样:
使用Java构建流程图形是很乏味的事情, 因为你必须在局部变量中跟踪所有的引用。 为了解决这个问题,流程虚拟机提供了一个ProcessFactory。 ProcessFactory是一种领域特定语言(DSL),可以嵌入到Java中, 简化流程图形的结构。这个模型也叫做 流畅接口。
ClientProcessDefinition processDefinition = ProcessFactory.build("loan")
.activity("submit loan request").initial().behaviour(new Display("loan request submitted"))
.transition().to("evaluate")
.activity("evaluate").behaviour(new WaitState())
.transition("approve").to("wire money")
.transition("reject").to("end")
.activity("wire money").behaviour(new Display("wire the money"))
.transition().to("archive")
.activity("archive").behaviour(new WaitState())
.transition().to("end")
.activity("end").behaviour(new WaitState())
.done();
为了了解ProcessFactory的更多细节,可以参考
api文档。
ProcessFactory的另一种选择是创建一个XML语言和一个XML解析器,来表示流程。
XML解析器可以直接实例化
org.jbpm.pvm.internal.model包中的类。
这种方式一般都被流程语言选择使用。
初始化活动submit loan request和
wire the money活动是自动活动。
在这个例子中,wire the money活动的
Display实现
使用Java API来把信息输出到控制台上。但是读取器可以想象一个可选的
Activity实现,使用支付流程库的Java API
来实现一个真实的自动支付。
上述流程的一个新执行可以像下面这样启动
ClientExecution execution = processDefinition.startProcessInstance();
当startExecution方法返回时,
submit loan request活动会被执行,
执行会位于evaluate活动。
现在,执行处在一个很有趣的点。这里有两个转移从evaluate指向外边。
一个转移叫approve
一个转移叫reject。像我们上面解释的,
WaitState实现会根据执行的signal选择转移。
让我们像这样执行'approve' signal:
execution.signal("approve");
这个approve signal会导致执行选择approve转移
它会到达wire money活动。
在wire money活动中,信息会打印到控制台里。
因为Display没有调用execution.waitForSignal(),
也没有调用其他执行传播方法,
默认流程行为只会让执行继续,
使用向外的转移到达archive活动,
这也是一个WaitState。
所以只有当archive到达时,
signal("approve")会返回。
另一个signal就像这样:
execution.signal("approve");
将让执行最终到达结束状态。
事件位于流程定义中,
一系列的EventListener可以进行注册。
public interface EventListener extends Serializable {
void notify(EventListenerExecution execution) throws Exception;
}
事件的目的是让开发者可以为流程添加程序逻辑, 不必改变流程图。 这是非常有价值的机制,可以促进业务分析人员和开发者之间的协作。 业务分析人员负责描述需求。 当他们使用流程图归档那些需求, 开发者可以获得这些图形,让它可执行化。 事件会非常方便,向一个流程中添加技术细节(比如一些数据库插入操作) 这些都是业务分析人员不感兴趣的东西。
最常用的事件是由执行自动触发的:
TODO: 在用户手册中解释事件
事件是由流程元素和事件名称结合而成。 用户和流程语言也可以出发事件, 使用编程的方式在流程中使用fire方法。
public interface Execution extends Serializable {
...
void fire(String eventName, ProcessElement eventSource);
...
}
可以把一系列的EventListeners分配给一个事件。
但是事件监听器不能控制执行的流向,
因为它们仅仅是监听已经执行了的执行。
这与活动处理活动的行为是不同的。
活动行为可以响应执行的传播。
我们会创建一个PrintLn事件监听器,
这与上面的Display活动是非常相似的。
public class PrintLn implements EventListener {
String message;
public PrintLn(String message) {
this.message = message;
}
public void notify(EventListenerExecution execution) throws Exception {
System.out.println("message");
}
}
多个PrintLn监听器
会在流程中注册。
ClientProcessDefinition processDefinition = ProcessFactory.build()
.activity("a").initial().behaviour(new AutomaticActivity())
.event("end")
.listener(new PrintLn("leaving a"))
.listener(new PrintLn("second message while leaving a"))
.transition().to("b")
.listener(new PrintLn("taking transition"))
.activity("b").behaviour(new WaitState())
.event("start")
.listener(new PrintLn("entering b"))
.done();
第一个事件演示如何为相同的事件注册多个监听器。 它们会根据它们指定的顺序依次执行。
然后,在转移中只有一种类型的事件。 所以在那种情况下,事件类型不需要指定, 监听器可以直接添加到转移上。
一个监听器每次都会执行,当一个执行触发事件时,如果这个监听器被注册了。 执行会作为一个参数提供给活动接口, 除了控制流程传播的方法以外, 都可以被监听器使用。
事件会默认传播给最近的流程元素。
目的是允许监听器在流程定义或组合活动中
可以执行所有发生在流程元素中的事件。
比如这个功能允许为end事件在流程定义或一个组合活动中注册一个事件监听器。
这种动作会被执行,如果一个活动离开。
如果事件监听器被注册到一个组合活动中,
它也会被所有活动执行,当组合活动中出现了离开事件。
为了清楚地显示这个,我们会创建一个DisplaySource事件监听器,
这会把leaving信息和事件源
打印到控制台。
public class DisplaySource implements EventListener { public void execute(EventListenerExecution execution) { System.out.println("leaving "+execution.getEventSource()); } }
注意事件监听器的目的不是可视化,这是为什么事件监听器本身
不应该显示在图形中。一个DisplaySource事件监听器
会作为end事件的监听器添加到组合活动中。
下一个流程展示了DisplaySource事件监听器如何
作为'end'事件的监听器注册到composite活动:
TODO 更新代码片段
下一步,我们会启动一个执行。
ClientExecution execution = processDefinition.startProcessInstance();
在启动一个新执行后,执行将在a活动中
作为初始活动。没有活动离开,所以没有信息被记录下来。
下一个signal会给与执行,
导致它选择从a到b。
execution.signal();
当signal方法返回,执行会选择转移
然后end事件会被a活动触发。
那个组合活动会被传播到组合活动和流程定义中。
因为我们的DisplaySource
监听器放到
composite活动中,
它会接收事件,把下面的信息打印到控制台中:
leaving activity(a)
另一个
execution.signal();
会选择b到c的转移。那会触发两个活动离开事件。 一个在b活动,一个在组合活动。 所以下面的几行会添加到控制台输出中:
leaving activity(b) leaving activity(composite)
事件传播建立在流程定义的继承组合结构中。 顶级元素总是流程定义。 流程定义包含一系列活动。每个活动可以是叶子活动或者可以是一个组合节点, 这意味着它包含了一系列内嵌活动。 内嵌活动可以被使用,比如超级状态或组合活动,在内嵌流程语言中,像BPEL。
所以事件模型在组合活动和上面的流程定义中的功能是相似的。 想象'Phase one'模型一个超级状态作为一个状态机。 然后事件传播允许在超级状态中注册所有事件。 这个主意是继承组合响应图形展示。 如果一个'e'元素画在另一个'p'元素中, 'p'是'e'的父节点。一个流程定义拥有一系列定义活动。 每个活动可以拥有一系列内嵌活动。 一个转移的父节点就是它的源头和目的的第一个父节点。
如果一个事件监听器对传播的事件没有兴趣,
可以在构建流程使用ProcessFactory的propagationDisabled()。
下一个流程是与上面相同的流程,
除了传播的事件会被事件监听器禁用。
图形还是一样。
使用流程工厂构建流程:
TODO 更新代码
所以当第一个signal在流程中调用时,end事件
会再次触发在a活动上,但是现在在组合活动的事件监听器
不会被执行,因为传播的事件被禁用了。
禁用传播是单独的事件监听器的一个属性,
不会影响其他监听器。事件会一直被触发,
传播到整个父继承结构。
ClientExecution execution = processDefinition.startProcessInstance();
第一个signal会选择从a到b的流程。
没有信息会被打印到控制台。
execution.signal();
下一步,第二个signal会选择从b到c的转移。
execution.signal()
还是两个end事件被触发,
就像上面分别在b和composite活动中。
第一个事件是b活动上的
end事件。
那将被传播给composite活动。
所以事件监听器不会为这个事件执行,因为它已经禁用了传播。
但是事件监听器会在composite活动上
为end事件执行。
那是不传播的,但是直接在composite活动上触发。
所以事件监听器现在会被执行
一次,为组合活动,就像下面控制台里显示的那样:
leaving activity(composite)