本内容参考官网:官网文档
1 活动
事件用于模拟在流程生命周期中发生的事情。事件总是可视化为圆圈。在BPMN 2.0中,存在两个主要事件类别:捕获和抛出事件。
- **捕获:**当流程执行到达事件时,它将等待触发发生。触发器的类型由XML中的内部图标或类型声明定义。通过未填充的内部图标(它只是白色)在视觉上区分捕捉事件。
- **投掷:**当进程执行到达事件时,触发器被触发。触发器的类型由XML中的内部图标或类型声明定义。通过填充黑色的内部图标在视觉上区分投掷事件。
1.1.1 事件定义
事件定义定义事件的语义。没有事件定义,事件“没有什么特别之处”。例如,没有事件定义的启动事件没有指定究竟启动进程的内容。如果我们向start事件添加一个事件定义(例如,一个timer事件定义),我们声明事件的“type”启动过程(在一个定时器事件定义的情况下,某个时间点的事实是到达)。
1.1.2 定时器事件定义
计时器事件是由定义的计时器触发的事件。它们可以用作开始事件,中间事件或边界事件。时间事件的行为取决于使用的业务日历。每个计时器事件都有一个默认的业务日历,但业务日历也可以作为计时器事件定义的一部分给出。
<timerEventDefinition flowable:businessCalendarName="custom">
...
</timerEventDefinition>
其中businessCalendarName指向流程引擎配置中的业务日历。省略业务日历时,将使用默认业务日历。
计时器定义必须具有以下一个元素:
- timeDate。此格式以ISO 8601格式指定固定日期,此时将触发触发器。例如:
<timerEventDefinition>
<timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
- timeDuration。要指定计时器在触发之前应运行多长时间,可以将timeDuration指定为timerEventDefinition的子元素。使用的格式是ISO 8601格式(根据BPMN 2.0规范的要求)。例如(间隔持续10天):
<timerEventDefinition>
<timeDuration>P10D</timeDuration>
</timerEventDefinition>
- timeCycle。指定重复间隔,这对于定期启动进程或为过期用户任务发送多个提醒非常有用。时间周期元素可以是两种格式之一。首先,是ISO 8601标准规定的经常性持续时间的格式。示例(3个重复间隔,每个持续10个小时):
也可以将endDate指定为timeCycle上的可选属性,或者在时间表达式的末尾指定,如下所示:R3/PT10H/${EndDate}
。到达endDate时,应用程序将停止为此任务创建其他作业。它接受静态值ISO 8601标准的值,例如*“2015-02-25T16:42:11 + 00:00”,或变量,例如$ {EndDate}*
<timerEventDefinition>
<timeCycle flowable:endDate="2015-02-25T16:42:11+00:00">R3/PT10H</timeCycle>
</timerEventDefinition>
<timerEventDefinition>
<timeCycle>R3/PT10H/${EndDate}</timeCycle>
</timerEventDefinition>
如果同时指定了两者,则系统将使用指定为属性的endDate。
目前,只有BoundaryTimerEvents和CatchTimerEvent支持EndDate功能。
此外,您可以使用cron表达式指定时间周期; 以下示例显示从完整小时开始每5分钟触发一次:
0 0/5 * * *?
请参阅本教程以了解如何使用cron表达式。
**注意:**第一个符号表示秒,而不是正常Unix cron中的分钟。
循环持续时间更适合处理相对定时器,相对于某个特定时间点(例如,用户任务启动的时间)计算相对定时器,而cron表达式可以处理绝对定时器,这对于计时器启动事件。
您可以将表达式用于计时器事件定义,通过这样做,您可以根据流程变量影响计时器定义。对于适当的计时器类型,过程变量必须包含ISO 8601(或循环类型的cron)字符串。另外,对于持续时间,java.time.Duration
可以使用返回的类型或表达式的变量。
<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>${duration}</timeDuration>
</timerEventDefinition>
</boundaryEvent>
**注:**计时器,只有当启用了异步执行(烧制asyncExecutorActivate必须设置为true
中flowable.cfg.xml
,因为异步执行默认情况下禁用)。
1.1.3 错误事件定义
重要说明: BPMN错误与Java异常不同。事实上,两者没有任何共同之处。BPMN错误事件是一种建模业务异常的方法。Java异常以其自己的特定方式处理。
<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
1.1.4 信号事件定义
信号事件是引用命名信号的事件。信号是全局范围的事件(广播语义),并传递给所有活动的处理程序(等待进程实例/捕获信号事件)。
使用该signalEventDefinition
元素声明信号事件定义。该属性signalRef
引用signal
声明为definitions
根元素的子元素的元素。以下是一个过程的摘录,其中信号事件被中间事件抛出并捕获。
<definitions... >
<!-- declaration of the signal -->
<signal id="alertSignal" name="alert" />
<process id="catchSignal">
<intermediateThrowEvent id="throwSignalEvent" name="Alert">
<!-- signal event definition -->
<signalEventDefinition signalRef="alertSignal" />
</intermediateThrowEvent>
...
<intermediateCatchEvent id="catchSignalEvent" name="On Alert">
<!-- signal event definition -->
<signalEventDefinition signalRef="alertSignal" />
</intermediateCatchEvent>
...
</process>
</definitions>
所述signalEventDefinition
参考中相同的signal
元件。
投掷信号事件
信号可以由流程实例使用BPMN构造抛出,也可以使用java API以编程方式抛出。以下方法org.flowable.engine.RuntimeService
可用于以编程方式抛出信号:
RuntimeService.signalEventReceived(String signalName);
RuntimeService.signalEventReceived(String signalName, String executionId);
signalEventReceived(String signalName)
和之间的区别在于signalEventReceived(String signalName, String executionId)
第一种方法将信号全局抛出到所有预订处理程序(广播语义),第二种方法仅将信号传递给特定执行。
捕捉信号事件
信号事件可以被中间捕获信号事件或信号边界事件捕获。
查询信号事件订阅
可以查询已订阅特定信号事件的所有执行:
List<Execution> executions = runtimeService.createExecutionQuery()
.signalEventSubscriptionName("alert")
.list();
然后我们可以使用该signalEventReceived(String signalName, String executionId)
方法将信号传递给这些执行。
信号事件范围
默认情况下,信号是广播流程引擎。这意味着您可以在流程实例中抛出信号事件,而具有不同流程定义的其他流程实例可以对此事件的发生做出反应。
但是,有时需要仅在同一个流程实例中对信号事件作出反应。例如,用例是当两个或多个活动互斥时流程实例中的同步机制。
要限制信号事件的范围,请将(非BPMN 2.0标准!)范围属性添加到信号事件定义中:
<signal id="alertSignal" name="alert" flowable:scope="processInstance"/>
此属性的默认值为*“global”*。
信号事件示例
以下是使用信号进行通信的两个独立进程的示例。如果更新或更改保险单,则启动第一个流程。在人类参与者审查了更改后,将抛出信号事件,表明策略已更改:
现在,所有感兴趣的流程实例都可以捕获此事件。以下是订阅该事件的流程示例。
注意:了解信号事件向所有活动处理程序广播非常重要。这意味着,在上面给出的示例的情况下,捕获信号的过程的所有实例将接收事件。在这种情况下,这就是我们想要的。但是,也存在无意中广播行为的情况。请考虑以下过程:
BPMN不支持上述过程中描述的模式。这个想法是,执行“执行某事”任务时抛出的错误被边界错误事件捕获,使用信号throw事件传播到并行执行路径,然后中断“并行执行”任务。到目前为止,Flowable将按预期执行。信号将传播到捕获边界事件并中断任务。**但是,由于信号的广播语义,它还将传播到已订阅信号事件的所有其他进程实例。**在这种情况下,这可能不是我们想要的。
注意: signal事件不会对特定流程实例执行任何类型的关联。相反,它会广播到所有流程实例。如果只需要向特定流程实例发送信号,请手动执行关联并signalEventReceived(String signalName, String executionId)
与相应的查询机制一起使用。
Flowable确实有办法通过将scope属性添加到设置为processInstance的signal事件来解决此问题。
1.1.5 消息事件定义
消息事件是引用命名消息的事件。消息具有名称和有效负载。与信号不同,消息事件始终指向单个接收器。
使用该messageEventDefinition
元素声明消息事件定义。该属性messageRef
引用message
声明为definitions
根元素的子元素的元素。以下是一个过程的摘录,其中两个消息事件由start事件和中间捕获消息事件声明和引用。
<definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples"
xmlns:tns="Examples">
<message id="newInvoice" name="newInvoiceMessage" />
<message id="payment" name="paymentMessage" />
<process id="invoiceProcess">
<startEvent id="messageStart" >
<messageEventDefinition messageRef="newInvoice" />
</startEvent>
...
<intermediateCatchEvent id="paymentEvt" >
<messageEventDefinition messageRef="payment" />
</intermediateCatchEvent>
...
</process>
</definitions>
抛出一个消息事件
作为可嵌入的流程引擎,Flowable并不关心实际接收消息。这将取决于环境并且需要特定于平台的活动,例如连接到JMS(Java消息服务)队列/主题或处理Web服务或REST请求。因此,接收消息是您必须实现的过程引擎嵌入的应用程序或基础结构的一部分。
在应用程序中收到消息后,您必须决定如何处理它。如果消息应触发新流程实例的启动,请在运行时服务提供的以下方法之间进行选择:
ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey,
Map<String, Object> processVariables);
这些方法使用引用的消息启动流程实例。
如果消息需要由现有流程实例接收,则首先必须将消息关联到特定流程实例(请参阅下一节),然后触发等待执行的继续。运行时服务提供以下方法,用于根据消息事件订阅触发执行:
void messageEventReceived(String messageName, String executionId);
void messageEventReceived(String messageName, String executionId, HashMap<String, Object> processVariables);
查询消息事件订阅
- 在消息启动事件的情况下,消息事件订阅与特定的流程定义相关联。可以使用以下命令查询此类消息订阅
ProcessDefinitionQuery
:
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.messageEventSubscription("newCallCenterBooking")
.singleResult();
由于特定邮件订阅只能有一个流程定义,因此查询始终返回零或一个结果。如果更新了流程定义,则只有最新版本的流程定义才能订阅消息事件。
- 在中间捕获消息事件的情况下,消息事件订阅与特定执行相关联。可以使用以下命令查询此类消息事件订阅
ExecutionQuery
:
Execution execution = runtimeService.createExecutionQuery()
.messageEventSubscriptionName("paymentReceived")
.variableValueEquals("orderId", message.getOrderId())
.singleResult();
此类查询称为相关查询,通常需要有关进程的知识(在这种情况下,给定orderId最多只有一个流程实例)。
消息事件示例
以下是可以使用两个不同消息启动的进程示例:
如果流程需要替代方法来响应不同的启动事件,但最终以统一的方式继续,这将非常有用。
1.1.6 开始活动
开始事件表示进程的开始位置。类型开始事件(处理开始于消息的到达,在特定的时间间隔,等等),定义如何在处理开始时,被示出为在事件的视觉表示一个小图标。在XML表示中,类型由子元素的声明给出。
开始事件总是捕获:从概念上讲,事件(在任何时候)等待直到某个触发发生。
在start事件中,可以指定以下Flowable特定属性:
- initiator:标识进程启动时将在其中存储经过身份验证的用户ID的变量名称。例如:
<startEvent id="request" flowable:initiator="initiator" />
必须使用IdentityService.setAuthenticatedUserId(String)
try-finally块中的方法设置经过身份验证的用户,如下所示:
try {
identityService.setAuthenticatedUserId("bono");
runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
identityService.setAuthenticatedUserId(null);
}
此代码已粘贴到Flowable应用程序中,因此它与Forms结合使用。
1.1.7 无开始活动
描述
一个没有启动事件技术上意味着启动过程的实例的触发是不确定的。这意味着引擎无法预测何时必须启动流程实例。通过调用startProcessInstanceByXXX方法之一,通过API启动流程实例时,将使用none start事件。
ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();
*注意:*子进程始终具有无启动事件。
图形符号
无启动事件可视化为没有内部图标的圆圈(换句话说,没有触发类型)。
XML表示
无启动事件的XML表示是没有任何子元素的正常启动事件声明(其他启动事件类型都具有声明该类型的子元素)。
<startEvent id="start" name="my start event" />
无启动事件的自定义扩展
formKey:引用用户在启动新流程实例时必须填写的表单定义。更多信息可以在表单部分找到示例:
<startEvent id="request" flowable:formKey="request" />
1.1.8 定时器启动事件
描述
计时器启动事件用于在给定时间创建流程实例。它既可以用于应该只启动一次的进程,也可以用于应该以特定时间间隔启动的进程。
*注意:*子进程不能有计时器启动事件。
*注意:*一旦部署了进程,就会安排启动计时器事件。不需要调用startProcessInstanceByXXX,虽然调用start process方法不受限制,并且会在startProcessInstanceByXXX调用时再引发一个进程。
*注意:*当部署具有启动计时器事件的新版本的进程时,将删除与先前计时器对应的作业。原因是通常不希望自动启动旧版本流程的新流程实例。
图形符号
计时器启动事件可视化为带有时钟内部图标的圆圈。
XML表示
计时器启动事件的XML表示是具有计时器定义子元素的正常启动事件声明。有关配置详细信息,请参阅计时器定义。
示例:从2011年3月11日12:13开始,过程将以5分钟为间隔开始4次
<startEvent id="theStart">
<timerEventDefinition>
<timeCycle>R4/2011-03-11T12:13/PT5M</timeCycle>
</timerEventDefinition>
</startEvent>
示例:进程将在所选日期开始一次
<startEvent id="theStart">
<timerEventDefinition>
<timeDate>2011-03-11T12:13:14</timeDate>
</timerEventDefinition>
</startEvent>
1.1.9 消息开始事件
描述
一个消息开始事件可用于使用已命名的信息来启动一个过程实例。这有效地允许我们使用消息名称从一组备选启动事件中选择正确的启动事件。
在使用一个或多个消息启动事件部署流程定义时,以下注意事项适用:
- 消息启动事件的名称在给定的流程定义中必须是唯一的。流程定义不能具有多个具有相同名称的消息启动事件。Flowable在部署包含两个或多个引用相同消息的消息启动事件的流程定义时抛出异常,或者两个或多个消息启动事件引用具有相同消息名称的消息时抛出异常。
- 消息启动事件的名称在所有已部署的流程定义中必须是唯一的。Flowable在部署包含一个或多个消息启动事件的流程定义时抛出异常,该消息启动事件引用与已由不同流程定义部署的消息启动事件同名的消息。
- 流程版本控制:部署新版本的流程定义后,将删除先前版本的启动消息订阅。
当启动一个流程实例,可以使用在下面的方法来触发的消息开始的事件RuntimeService
:
ProcessInstance startProcessInstanceByMessage(String messageName);
ProcessInstance startProcessInstanceByMessage(String messageName, Map<String, Object> processVariables);
ProcessInstance startProcessInstanceByMessage(String messageName, String businessKey,
Map<String, Object< processVariables);
的messageName
是在给定的名字name
的属性message
由引用元素messageRef
的属性messageEventDefinition
。启动流程实例时,以下注意事项适用:
- 消息启动事件仅在顶级进程上受支持。嵌入式子进程不支持消息启动事件。
- 如果流程定义具有多个消息启动事件,则
runtimeService.startProcessInstanceByMessage(…)
允许选择适当的启动事件。 - 如果一个流程定义有多个消息开始的事件和一个没有启动的事件,
runtimeService.startProcessInstanceByKey(…)
或者runtimeService.startProcessInstanceById(…)
使用无启动事件启动流程实例。 - 如果一个流程定义有多个消息开始的事件,并没有没有启动事件,
runtimeService.startProcessInstanceByKey(…)
或者runtimeService.startProcessInstanceById(…)
抛出一个异常。 - 如果一个流程的定义有一个消息开始的事件,
runtimeService.startProcessInstanceByKey(…)
或者runtimeService.startProcessInstanceById(…)
使用消息开始的事件开始一个新的流程实例。 - 如果从引用活动启动进程,则仅支持消息启动事件
- 除了消息启动事件之外,该进程还有一个无启动事件
- 该进程有一个消息启动事件,没有其他启动事件。
图形符号
消息开始事件可视化为带有消息事件符号的圆圈。符号未填充,表示捕获(接收)行为。
XML表示
消息启动事件的XML表示是带有messageEventDefinition子元素的正常启动事件声明:
<definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples"
xmlns:tns="Examples">
<message id="newInvoice" name="newInvoiceMessage" />
<process id="invoiceProcess">
<startEvent id="messageStart" >
<messageEventDefinition messageRef="tns:newInvoice" />
</startEvent>
...
</process>
</definitions>
1.1.10 信号启动事件
描述
一个信号启动事件可用于使用命名信号来启动一个过程实例。可以使用中间信号throw事件或通过API(runtimeService.signalEventReceivedXXX方法)从流程实例中触发信号。在这两种情况下,将启动具有相同名称的信号启动事件的所有流程定义。
请注意,在这两种情况下,还可以在流程实例的同步和异步启动之间进行选择。
在signalName
必须在API中传递是在给定的名称name
的属性signal
被引用的元素signalRef
的属性signalEventDefinition
。
图形符号
信号开始事件可视化为具有信号事件符号的圆圈。符号未填充,表示捕获(接收)行为。
XML表示
信号启动事件的XML表示是具有signalEventDefinition子元素的正常启动事件声明:
<signal id="theSignal" name="The Signal" />
<process id="processWithSignalStart1">
<startEvent id="theStart">
<signalEventDefinition id="theSignalEventDefinition" signalRef="theSignal" />
</startEvent>
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
<userTask id="theTask" name="Task in process A" />
<sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
1.1.11 错误开始事件
描述
一个错误启动事件可用于触发事件的子过程。错误启动事件不能用于启动流程实例。
错误启动事件总是在中断。
图形符号
错误开始事件可视化为带有错误事件符号的圆圈。符号未填充,表示捕获(接收)行为。
XML表示
错误启动事件的XML表示形式是带有errorEventDefinition子元素的正常启动事件声明:
<startEvent id="messageStart" >
<errorEventDefinition errorRef="someError" />
</startEvent>
1.1.12 结束事件
结束事件表示流程或子流程中路径的结束。终结事件总是在抛出。这意味着当流程执行到达结束事件时,将抛出结果。结果的类型由事件的内部黑色图标描绘。在XML表示中,类型由子元素的声明给出。
1.1.13 无结束事件
描述
一个没有终点事件意味着结果当达到该事件是不确定的抛出。因此,除了结束当前的执行路径之外,引擎不会做任何额外的事情。
图形符号
无端事件可视化为具有粗边框但没有内部图标(无结果类型)的圆。
XML表示
无结束事件的XML表示是正常的结束事件声明,没有任何子元素(其他结束事件类型都有一个声明该类型的子元素)。
<endEvent id="end" name="my end event" />
1.1.14 错误结束事件
描述
当流程执行到达错误结束事件时,当前执行路径结束并抛出错误。匹配的中间边界错误事件可能会捕获此错误。如果未找到匹配的边界错误事件,则将引发异常。
图形符号
错误结束事件可视化为典型的结束事件(具有粗边框的圆圈),其中包含错误图标。错误图标完全是黑色,表示其抛出语义。
XML表示
错误结束事件表示为结束事件,带有errorEventDefinition子元素。
<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
所述errorRef属性可以引用错误指在过程之外定义元件:
<error id="myError" errorCode="123" />
...
<process id="myProcess">
...
该的errorCode的的错误将用于查找匹配的捕捉边界错误事件。如果errorRef与任何已定义的错误不匹配,则errorRef将用作errorCode的快捷方式。这是一个Flowable特定的快捷方式。更具体地说,以下片段在功能上是等同的。
<error id="myError" errorCode="error123" />
...
<process id="myProcess">
...
<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>
...
相当于
<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="error123" />
</endEvent>
请注意,errorRef必须符合BPMN 2.0模式,并且必须是有效的QName。
1.1.5 终止结束事件
描述
当达到终止结束事件时,将终止当前流程实例或子流程。从概念上讲,当执行到达终止结束事件时,将确定并结束第一范围(流程或子流程)。请注意,在BPMN 2.0中,子流程可以是嵌入式子流程,调用活动,事件子流程或事务子流程。此规则通常适用:例如,当存在多实例调用活动或嵌入式子流程时,仅该实例将结束,其他实例和流程实例不受影响。
可以添加可选属性terminateAll。如果为true,则无论在流程定义中是否放置终止结束事件,并且无论是否处于子流程(甚至是嵌套),(根)流程实例都将终止。
图形符号
取消结束事件可视化为典型的结束事件(具有粗轮廓的圆圈),内部带有完整的黑色圆圈。
XML表示
终止事件事件表示为end事件,具有terminateEventDefinition子元素。
请注意,terminateAll属性是可选的(默认情况下为false)。
<endEvent id="myEndEvent >
<terminateEventDefinition flowable:terminateAll="true"></terminateEventDefinition>
</endEvent>
1.1.16 取消结束事件
描述
取消结束事件只能与BPMN事务子流程结合使用。当到达取消结束事件时,抛出取消事件,必须由取消边界事件捕获。取消边界事件然后取消交易并触发补偿。
图形符号
取消结束事件可视化为典型的结束事件(具有粗轮廓的圆圈),其中包含取消图标。取消图标完全是黑色,表示其抛出语义。
XML表示
取消结束事件表示为结束事件,具有cancelEventDefinition子元素。
<endEvent id="myCancelEndEvent">
<cancelEventDefinition />
</endEvent>
1.1.17 边界事件
边界事件捕获附加到活动的事件(边界事件永远不会抛出)。这意味着当活动正在运行时,事件正在侦听某种类型的触发器。捕获事件时,活动将中断,并且将遵循从事件中退出的序列流。
所有边界事件都以相同的方式定义:
<boundaryEvent id="myBoundaryEvent" attachedToRef="theActivity">
<XXXEventDefinition/>
</boundaryEvent>
边界事件定义为
- 唯一标识符(流程范围内)
- 通过attachedToRef属性引用事件的活动的引用。请注意,边界事件的定义与它们所附加的活动处于同一级别(换句话说,在活动中不包含边界事件)。
- 形式为XXXEventDefinition的XML子元素(例如,TimerEventDefinition,ErrorEventDefinition等),用于定义边界事件的类型。有关详细信息,请参阅特定边界事件类型。
1.1.18 定时器边界事件
描述
计时器边界事件充当秒表和闹钟。当执行到达附加边界事件的活动时,启动计时器。当计时器触发时(例如,在指定的间隔之后),活动被中断并且遵循边界事件之外的顺序流。
图形符号
计时器边界事件可视化为典型的边界事件(边界上的圆圈),内部有计时器图标。
XML表示
计时器边界事件被定义为常规边界事件。在这种情况下,特定类型的子元素是timerEventDefinition元素。
<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>PT4H</timeDuration>
</timerEventDefinition>
</boundaryEvent>
有关定时器配置的详细信息,请参阅定时器事件定义。
在图形表示中,圆圈的线条点缀,如上例所示:
典型的用例是在一段时间后发送升级电子邮件,但不会影响正常的流程。
中断和非中断定时器事件之间存在关键差异。不间断意味着原始活动不会中断但保持原样。中断行为是默认行为。在XML表示中,cancelActivity属性设置为false:
<boundaryEvent id="escalationTimer" cancelActivity="false" attachedToRef="firstLineSupport"/>
**注:**边界计时器事件仅当启用了异步执行发生(asyncExecutorActivate需要被设置为true
在flowable.cfg.xml
,因为异步执行默认情况下禁用)。
边界事件的已知问题
在使用任何类型的边界事件时,存在关于并发性的已知问题。目前,不可能将多个传出序列流附加到边界事件。该问题的解决方案是使用一个传递到并行网关的传出序列流。
1.1.19 错误边界事件
描述
活动边界上的中间捕获错误或简称边界错误事件捕获在定义它的活动范围内引发的错误。
定义边界错误事件对嵌入式子流程或调用活动最有意义,因为子流程为子流程内的所有活动创建范围。错误结束事件引发错误。这样的错误将向上传播其父作用域,直到找到定义了与错误事件定义匹配的边界错误事件的作用域。
捕获错误事件时,将销毁定义边界事件的活动,同时销毁(并发活动,嵌套子流程等)中的所有当前执行。在边界事件的输出序列流之后继续执行流程。
图形符号
边界错误事件可视化为边界上的典型中间事件(内部具有较小圆圈的圆圈),其中包含错误图标。错误图标为白色,表示其捕获语义。
XML表示
边界错误事件被定义为典型的边界事件:
<boundaryEvent id="catchError" attachedToRef="mySubProcess">
<errorEventDefinition errorRef="myError"/>
</boundaryEvent>
与错误结束事件一样,errorRef引用在process元素外部定义的错误:
<error id="myError" errorCode="123" />
...
<process id="myProcess">
...
该的errorCode用来匹配被发现的错误:
- 如果errorRef被省略,边界错误事件将捕获任何错误事件,而不管errorCode的错误。
- 如果提供了errorRef并且它引用了现有错误,则边界事件将仅捕获具有相同错误代码的错误。
- 如果提供了errorRef,但BPMN 2.0文件中没有定义错误,则errorRef用作errorCode(类似于错误结束事件)。
例
以下示例流程显示了如何使用错误结束事件。当通过没有提供足够的信息来完成*“审查盈利能力”用户任务时,会引发错误。如果在子流程的边界上发现此错误,则“审核销售线索”子流程中的所有活动活动都将被销毁(即使“审核客户评级”尚未完成),并且“提供其他详细信息” '*用户任务已创建。
此过程在演示设置中作为示例提供。可以在org.flowable.examples.bpmn.event.error包中找到进程XML和单元测试。
1.1.20 信号边界事件
描述
在活动边界上的附加中间捕获 信号或简称边界信号事件捕获具有与参考信号定义相同的信号名称的信号。
**注意:**与其他事件(例如边界错误事件)相反,边界信号事件不仅捕获从其附加的范围抛出的信号事件。相反,信号事件具有全局范围(广播语义),这意味着信号可以从任何地方抛出,甚至可以从不同的流程实例抛出。
**注意:**与其他事件(例如错误事件)相反,如果捕获了信号,则不会消耗该信号。如果有两个活动信号边界事件捕获相同的信号事件,则两个边界事件都会被触发,即使它们是不同流程实例的一部分。
图形符号
边界信号事件可视化为边界上的典型中间事件(内部具有较小圆的圆),其中具有信号图标。信号图标为白色(未填充),表示其捕获语义。
XML表示
边界信号事件被定义为典型的边界事件:
<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<signalEventDefinition signalRef="alertSignal"/>
</boundaryEvent>
例
请参阅有关信号事件定义的部分。
1.1.21 消息边界事件
描述
在活动边界上附加的中间捕获 消息或简称边界消息事件捕获具有与引用的消息定义相同的消息名称的消息。
图形符号
边界消息事件可视化为边界上的典型中间事件(内部具有较小圆圈的圆圈),其中包含消息图标。消息图标为白色(未填充),以指示其捕获语义。
请注意,边界消息事件可以是中断(右侧)和非中断(左侧)。
XML表示
边界消息事件被定义为典型的边界事件:
<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<messageEventDefinition messageRef="newCustomerMessage"/>
</boundaryEvent>
例
请参阅有关消息事件定义的部分。
1.1.22 取消边界事件
描述
当事务被取消时,触发事务子流程边界或简称边界取消事件的附加中间捕获取消事件。触发取消边界事件时,它首先中断当前作用域中的所有活动执行。接下来,它开始补偿交易范围内的所有有效补偿边界事件。补偿是同步进行的,换句话说,边界事件在离开交易之前等待补偿之前等待。当补偿完成时,使用在取消边界事件之外的任何序列流来保留事务子过程。
**注意:**事务子流程只允许一个取消边界事件。
**注意:**如果事务子流程承载嵌套的子流程,则仅对已成功完成的子流程触发补偿。
**注意:**如果在具有多实例特征的事务子流程上放置取消边界事件,则如果一个实例触发取消,则边界事件将取消所有实例。
图形符号
取消边界事件可视化为边界上的典型中间事件(内部具有较小圆圈的圆圈),其中具有取消图标。取消图标为白色(未填充),表示其捕获语义。
XML表示
取消边界事件被定义为典型的边界事件:
<boundaryEvent id="boundary" attachedToRef="transaction" >
<cancelEventDefinition />
</boundaryEvent>
由于取消边界事件始终在中断,因此cancelActivity
不需要该属性。
1.1.23 补偿边界事件
描述
附加的中间捕获的活动或边界上的补偿补偿边界事件对于短的,可以被用来补偿处理程序附加到活动。
补偿边界事件必须使用定向关联引用单个补偿处理程序。
补偿边界事件与其他边界事件具有不同的激活策略。其他边界事件(例如信号边界事件)在启动它们所附加的活动时被激活。活动结束后,将停用它们并取消相应的事件订阅。补偿边界事件是不同的。补偿边界事件在附加的活动成功完成时激活。此时,创建对补偿事件的相应订阅。在触发补偿事件或相应的流程实例结束时,将删除订阅。由此可见:
- 触发补偿时,与补偿边界事件关联的补偿处理程序的调用次数与成功完成的活动的次数相同。
- 如果将补偿边界事件附加到具有多个实例特征的活动,则会为每个实例创建补偿事件订阅。
- 如果补偿边界事件附加到循环内包含的活动,则每次执行活动时都会创建补偿事件订阅。
- 如果流程实例结束,则取消对补偿事件的订阅。
**注意:**嵌入式子流程不支持补偿边界事件。
图形符号
补偿边界事件可视化为边界上的典型中间事件(内部具有较小圆的圆),其中具有补偿图标。补偿图标为白色(未填充),表示其捕获语义。除了补偿边界事件之外,下图还显示了使用单向关联与边界事件关联的补偿处理程序:
XML表示
补偿边界事件被定义为典型的边界事件:
<boundaryEvent id="compensateBookHotelEvt" attachedToRef="bookHotel" >
<compensateEventDefinition />
</boundaryEvent>
<association associationDirection="One" id="a1"
sourceRef="compensateBookHotelEvt" targetRef="undoBookHotel" />
<serviceTask id="undoBookHotel" isForCompensation="true" flowable:class="..." />
由于在活动成功完成后激活补偿边界事件,因此cancelActivity
不支持该属性。
1.1.24 中间捕获事件
所有中间捕获事件都以相同的方式定义:
<intermediateCatchEvent id="myIntermediateCatchEvent" >
<XXXEventDefinition/>
</intermediateCatchEvent>
中间捕获事件定义为:
- 唯一标识符(流程范围内)
- 形式为XXXEventDefinition的XML子元素(例如,TimerEventDefinition),用于定义中间捕获事件的类型。有关详细信息,请参阅特定捕获事件类型。
1.1.25 定时器中间捕捉事件
描述
计时器中间事件充当秒表。当执行到达捕获事件活动时,启动计时器。当计时器触发时(例如,在指定的间隔之后),遵循从计时器中间事件发出的顺序流。
图形符号
计时器中间事件可视化为中间捕获事件,内部有计时器图标。
XML表示
计时器中间事件被定义为中间捕获事件。在这种情况下,特定类型子元素是timerEventDefinition元素。
<intermediateCatchEvent id="timer">
<timerEventDefinition>
<timeDuration>PT5M</timeDuration>
</timerEventDefinition>
</intermediateCatchEvent>
请参阅计时器事件定义以获取配置详
1.1.26 信号中间捕捉事件
描述
中间捕获 信号事件捕获具有与参考信号定义相同的信号名称的信号。
**注意:**与其他事件(例如错误事件)相反,如果捕获了信号,则不会消耗该信号。如果有两个活动信号边界事件捕获相同的信号事件,则两个边界事件都会被触发,即使它们是不同流程实例的一部分。
图形符号
中间信号捕获事件可视化为典型的中间事件(内部具有较小圆圈的圆圈),其中具有信号图标。信号图标为白色(未填充),表示其捕获语义。
XML表示
信号中间事件被定义为中间捕获事件。在这种情况下,特定类型子元素是signalEventDefinition元素。
<intermediateCatchEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateCatchEvent>
例
请参阅有关信号事件定义的部分。
1.1.27 消息中间捕获事件
描述
中间捕获 消息事件捕获具有指定名称的消息。
图形符号
中间捕获消息事件可视化为典型的中间事件(内部具有较小圆圈的圆圈),其中包含消息图标。消息图标为白色(未填充),以指示其捕获语义。
XML表示
消息中间事件被定义为中间捕获事件。在这种情况下,特定类型子元素是messageEventDefinition元素。
<intermediateCatchEvent id="message">
<messageEventDefinition signalRef="newCustomerMessage" />
</intermediateCatchEvent>
例
请参阅有关消息事件定义的部分。
1.1.28 中间抛出事件
所有中间抛出事件都以相同的方式定义:
<intermediateThrowEvent id="myIntermediateThrowEvent" >
<XXXEventDefinition/>
</intermediateThrowEvent>
中间抛出事件定义为:
- 唯一标识符(流程范围内)
- 形式为XXXEventDefinition的XML子元素(例如,signalEventDefinition),用于定义中间抛出事件的类型。有关详细信息,请参阅特定投掷事件类型。
1.1.29 中间抛出无事件
以下流程图显示了一个中间无事件的简单示例,该事件通常用于指示流程中实现的某些状态。
通过添加执行侦听器,这可以成为监视某些KPI的好钩子。
<intermediateThrowEvent id="noneEvent">
<extensionElements>
<flowable:executionListener class="org.flowable.engine.test.bpmn.event.IntermediateNoneEventTest$MyExecutionListener" event="start" />
</extensionElements>
</intermediateThrowEvent>
在这里,您可以添加一些自己的代码,以便将一些事件发送到您的BAM工具或DWH。在这种情况下,引擎本身不做任何事情,它只是通过。
1.1.30 信号中间抛出事件
描述
中间抛出 信号事件为定义的信号抛出信号事件。
在Flowable中,信号被广播给所有活动的处理程序(换句话说,所有捕获信号事件)。信号可以同步或异步发布。
- 在默认配置中,信号是同步传送的。这意味着抛出流程实例会等待信号传递到所有捕获流程实例。捕获流程实例也在与抛出流程实例相同的事务中得到通知,这意味着如果其中一个通知的实例产生技术错误(抛出异常),则所有涉及的实例都会失败。
- 信号也可以异步传送。在这种情况下,确定在达到抛出信号事件时哪些处理者是活动的。对于每个活动的处理程序,JobExecutor存储并传递异步通知消息(Job)。
图形符号
中间信号投掷事件可视化为典型的中间事件(内部具有较小圆圈的圆圈),其中包含信号图标。信号图标为黑色(填充),表示其抛出语义。
XML表示
信号中间事件被定义为中间抛出事件。在这种情况下,特定类型子元素是signalEventDefinition元素。
<intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateThrowEvent>
异步信号事件看起来像这样:
<intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" flowable:async="true" />
</intermediateThrowEvent>
例
请参阅有关信号事件定义的部分。
1.1.31 补偿中间抛出事件
描述
中间抛出补偿事件可用于触发补偿。
触发补偿:可以针对指定活动或托管补偿事件的范围触发补偿。通过执行与活动相关联的补偿处理程序来执行补偿。
- 当为活动抛出补偿时,相关的补偿处理程序执行的次数与活动成功完成的次数相同。
- 如果对当前范围进行补偿,则补偿当前范围内的所有活动,包括并发分支上的活动。
- 分层次地触发补偿:如果要补偿的活动是子流程,则对子流程中包含的所有活动触发补偿。如果子流程具有嵌套活动,则递归抛出补偿。但是,补偿不会传播到流程的“上层”:如果在子流程中触发补偿,则不会将补偿传播到子流程范围之外的活动。BPMN规范规定对“相同级别的子流程”的活动触发补偿。
- 在Flowable中,补偿以相反的执行顺序执行。这意味着最后完成的任何活动都会首先得到补偿,依此类推。
- 中间抛出补偿事件可用于补偿成功竞争的交易子过程。
**注意:**如果在包含子流程的范围内抛出补偿,并且子流程包含具有补偿处理程序的活动,则补偿仅在抛出补偿时成功完成后传播到子流程。如果嵌套在子流程中的某些活动已完成并附加了补偿处理程序,则如果尚未完成包含这些活动的子流程,则不会执行补偿处理程序。请考虑以下示例:
在这个过程中,我们有两个并发执行:一个执行嵌入式子流程,另一个执行“充值信用卡”活动。假设两个执行都已启动,并且第一个并发执行正在等待用户完成“审核预订”任务。第二次执行执行“充值信用卡”活动并抛出错误,这导致“取消预订”事件触发补偿。此时并行子流程尚未完成,这意味着补偿事件不会传播到子流程,因此不执行“取消酒店预订”补偿处理程序。如果用户任务(以及嵌入式子流程)在执行“取消预订”之前完成,
**流程变量:**当补偿嵌入的子流程时,用于执行补偿处理程序的执行可以访问子流程在子流程完成执行时所处的状态的本地流程变量。为实现此目的,采用与范围执行相关联的过程变量的快照(为执行子流程而创建的执行)。从这一点来看,有几个含义如下:
- 补偿处理程序无权访问添加到子流程范围内创建的并发执行的变量。
- 与层次结构中较高层次的执行相关联的流程变量(例如,与流程实例执行相关联的流程变量)不包含在快照中:补偿处理程序可以在抛出补偿时访问它们所处的状态中的这些流程变量。
- 变量快照仅用于嵌入式子流程,而不用于其他活动。
目前的限制:
waitForCompletion="false"
目前不受支持。当使用中间抛出补偿事件触发补偿时,仅在补偿成功完成后才会留下事件。- 补偿本身目前由并发执行执行。并发执行以补偿活动完成的相反顺序启动。
- 补偿不会传播到由调用活动生成的子流程实例。
图形符号
中间补偿抛出事件可视化为典型的中间事件(内部具有较小圆圈的圆圈),内部具有补偿图标。补偿图标为黑色(填充),表示其抛出语义。
XML表示
补偿中间事件被定义为中间抛出事件。在这种情况下,特定类型子元素是compensateEventDefinition元素。
<intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition />
</intermediateThrowEvent>
此外,可选参数activityRef
可用于触发特定范围或活动的补偿:
<intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition activityRef="bookHotel" />
</intermediateThrowEvent>
1.2 连线(序列流)
1.2.1 描述
连线是流程的两个元素之间的连接器。在流程执行期间访问元素后,将遵循所有传出的连线。这意味着BPMN 2.0的默认特性是并行:两个出线将创建两个独立的并行执行路径。
1.2.2 图形符号
连线可视化为从源元素朝向目标元素的箭头。箭头始终指向目标。
1.2.3 XML表示
连线需要具有进程唯一ID并引用现有源和目标元素。
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />
1.2.4 条件序列流
描述
序列流可以在其上定义条件。当只有BPMN 2.0活动时,默认行为是评估传出序列流的条件。当条件评估为true时,选择该出线。当以这种方式选择多个序列流时,将生成多个执行实例并且该流程将以并行方式继续执行。
**注意:**上述内容适用于BPMN 2.0活动(和事件),但不适用于网关。网关将根据网关类型以特定方式处理具有条件的顺序流。
图形符号
条件序列流可视化为规则序列流,开头有一个小钻石。条件表达式显示在序列流程旁边。
XML表示
条件序列流在XML中表示为常规序列流,包含conditionExpression子元素。请注意,目前仅支持tFormalExpressions,省略*xsi:type =“”*定义将默认为唯一受支持的表达式类型。
<sequenceFlow id="flow" sourceRef="theStart" targetRef="theTask">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
</sequenceFlow>
目前,conditionalExpressions 只能与UEL一起使用。有关这些的详细信息,请参阅“ 表达式 ”一节。使用的表达式应该解析为布尔值,否则在评估条件时会抛出异常。
- 下面的示例通过getter以典型的JavaBean样式引用流程变量的数据。
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.price > 100 && order.price < 250}]]>
</conditionExpression>
- 此示例调用解析为布尔值的方法。
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${order.isStandardOrder()}]]>
</conditionExpression>
Flowable分布包含使用值和方法表达式的以下示例流程(请参阅org.flowable.examples.bpmn.expression):
1.2.5 默认顺序流程
描述
所有BPMN 2.0任务和网关都可以具有默认的序列流。当且仅当不能选择其他序列流时,才将该序列流选择为该活动的输出序列流。始终忽略默认顺序流的条件。
图形符号
默认序列流可视化为常规序列流,开头带有斜杠标记。
XML表示
某个活动的默认顺序流由该活动的默认属性定义。以下XML片段显示了一个独占网关的示例,该网关具有默认顺序流,即流2。仅当conditionA和conditionB都评估为false时,才会选择它作为网关的传出顺序流。
<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" default="flow2" />
<sequenceFlow id="flow1" sourceRef="exclusiveGw" targetRef="task1">
<conditionExpression xsi:type="tFormalExpression">${conditionA}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="task2"/>
<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="task3">
<conditionExpression xsi:type="tFormalExpression">${conditionB}</conditionExpression>
</sequenceFlow>
这与以下图形表示相对应:
1.3 网关
网关用于控制执行流程(或者如BPMN 2.0描述的那样,执行的令牌)。网关能够消耗或生成令牌。
网关以图形方式显示为菱形,内部带有图标。该图标显示网关的类型。
1.3.1 排他网关
描述
排他网关(也称为XOR网关或更专业的基于数据的排他网关)用于对流程中的决策建模。当执行到达此网关时,将按照定义它们的顺序评估所有传出序列流。选择条件评估为真(或没有条件集,在概念上具有在序列流上定义的*'真'*)的第一序列流以继续该过程。
注意,在这种情况下,输出序列流的语义与BPMN 2.0中的一般情况的语义不同。通常,选择条件评估为真的所有序列流以并行方式继续,而在使用排他网关时仅选择一个序列流。如果多个序列流具有计算结果为true的条件,则选择XML中定义的第一个(并且只有那个!)以继续该过程。如果不能选择序列流,则会抛出异常。
图形符号
独家网关可视化为典型网关(菱形),内部带有X图标,指的是XOR语义。请注意,没有图标的网关默认为独占网关。BPMN 2.0规范不允许在同一流程定义中使用带有和不带X的菱形。
XML表示
专用网关的XML表示是直接的:定义网关的一行和在输出序列流上定义的条件表达式。请参阅有关条件序列流的部分,以了解哪些选项可用于此类表达式。
举例来说,以下模型:
其中用XML表示如下:
<exclusiveGateway id="exclusiveGw" name="Exclusive Gateway" />
<sequenceFlow id="flow2" sourceRef="exclusiveGw" targetRef="theTask1">
<conditionExpression xsi:type="tFormalExpression">${input == 1}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow3" sourceRef="exclusiveGw" targetRef="theTask2">
<conditionExpression xsi:type="tFormalExpression">${input == 2}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow4" sourceRef="exclusiveGw" targetRef="theTask3">
<conditionExpression xsi:type="tFormalExpression">${input == 3}</conditionExpression>
</sequenceFlow>
1.3.2 并行网关
描述
网关还可用于对流程中的并发进行建模。在流程模型中引入并发性的最直接的网关是并行网关,它允许您分叉到多个执行路径或连接多个传入的执行路径。
并行网关的功能基于传入和传出顺序流:
- **fork:**并行执行所有传出序列流,为每个序列流创建一个并发执行。
- **join:**到达并行网关的所有并发执行在网关中等待,直到每个传入的序列流都到达执行。然后,该过程继续经过加入网关。
请注意,如果同一并行网关有多个传入和传出顺序流,并行网关可以同时具有分叉和连接行为。在这种情况下,网关将首先连接所有传入的序列流,然后再分成多个并发的执行路径。
与其他网关类型的一个重要区别是并行网关不评估条件。如果在与并行网关连接的顺序流上定义条件,则忽略它们。
图形符号
并行网关可视化为网关(菱形),内部带有加号,引用AND语义。
XML表示
定义并行网关需要一行XML:
<parallelGateway id="myParallelGateway" />
实际行为(fork,join或两者)由连接到并行网关的顺序流定义。
例如,上面的模型归结为以下XML:
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />
<parallelGateway id="fork" />
<sequenceFlow sourceRef="fork" targetRef="receivePayment" />
<sequenceFlow sourceRef="fork" targetRef="shipOrder" />
<userTask id="receivePayment" name="Receive Payment" />
<sequenceFlow sourceRef="receivePayment" targetRef="join" />
<userTask id="shipOrder" name="Ship Order" />
<sequenceFlow sourceRef="shipOrder" targetRef="join" />
<parallelGateway id="join" />
<sequenceFlow sourceRef="join" targetRef="archiveOrder" />
<userTask id="archiveOrder" name="Archive Order" />
<sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />
<endEvent id="theEnd" />
在上面的示例中,在启动进程后,将创建两个任务:
ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
.processInstanceId(pi.getId())
.orderByTaskName()
.asc();
List<Task> tasks = query.list();
assertEquals(2, tasks.size());
Task task1 = tasks.get(0);
assertEquals("Receive Payment", task1.getName());
Task task2 = tasks.get(1);
assertEquals("Ship Order", task2.getName());
当这两个任务完成后,第二个并行网关将加入两个执行,并且由于只有一个传出序列流,因此不会创建并发执行路径,只有存档顺序任务才会处于活动状态。
注意,并行网关不需要平衡(对应的并行网关的输入/输出序列流的匹配数量)。并行网关将简单地等待所有传入的序列流,并为每个传出序列流创建并发执行路径,而不受流程模型中其他构造的影响。因此,以下过程在BPMN 2.0中是合法的:
1.3.3 包容网关
描述
可以把**包容网关(inclusive gateway)**看做排他网关与并行网关的组合。与排他网关一样,可以在包容网关的出口顺序流上定义条件,包容网关会计算条件。然而主要的区别是,包容网关与并行网关一样,可以同时选择多于一条出口顺序流。
包容网关的功能取决于其入口与出口顺序流:
- **分支:**流程会计算所有出口顺序流的条件。对于每一条计算为true的顺序流,流程都会创建一个并行执行。
- **合并:**所有到达包容网关的并行执行,都会在网关处等待。直到每一条具有流程标志(process token)的入口顺序流,都有一个执行到达。这是与并行网关的重要区别。换句话说,包容网关只会等待可以被执行的入口顺序流。在合并后,流程穿过合并并行网关继续。
请注意,如果包容网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有具有流程标志的入口顺序流,然后为每一个条件计算为true的出口顺序流分裂出并行执行路径。
包容网关的汇聚行为比并行网关更复杂。所有到达包容网关的并行执行,都会在网关等待,直到所有“可以到达”包容网关的执行都“到达”包容网关。 判断方法为:计算当前流程实例中的所有执行,检查从其位置是否有一条到达包容网关的路径(忽略顺序流上的任何条件)。如果存在这样的执行(可到达但尚未到达),则不会触发包容网关的汇聚行为。
图形符号
包含网关可视化为网关(菱形),内部带有圆圈符号。
XML表示
定义包含网关需要一行XML:
<inclusiveGateway id="myInclusiveGateway" />
实际行为(fork,join或两者)由连接到包含网关的顺序流定义。
例如,上面的模型归结为以下XML:
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="fork" />
<inclusiveGateway id="fork" />
<sequenceFlow sourceRef="fork" targetRef="receivePayment" >
<conditionExpression xsi:type="tFormalExpression">${paymentReceived == false}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="fork" targetRef="shipOrder" >
<conditionExpression xsi:type="tFormalExpression">${shipOrder == true}</conditionExpression>
</sequenceFlow>
<userTask id="receivePayment" name="Receive Payment" />
<sequenceFlow sourceRef="receivePayment" targetRef="join" />
<userTask id="shipOrder" name="Ship Order" />
<sequenceFlow sourceRef="shipOrder" targetRef="join" />
<inclusiveGateway id="join" />
<sequenceFlow sourceRef="join" targetRef="archiveOrder" />
<userTask id="archiveOrder" name="Archive Order" />
<sequenceFlow sourceRef="archiveOrder" targetRef="theEnd" />
<endEvent id="theEnd" />
在上面的示例中,在启动流程后,如果流程变量paymentReceived == false和shipOrder == true,则将创建两个任务。如果这些过程变量中只有一个等于true,则只创建一个任务。如果没有条件计算为true,则抛出异常。这可以通过指定默认的传出顺序流来防止。在以下示例中,将创建一个任务,即船舶订单任务:
HashMap<String, Object> variableMap = new HashMap<String, Object>();
variableMap.put("receivedPayment", true);
variableMap.put("shipOrder", true);
ProcessInstance pi = runtimeService.startProcessInstanceByKey("forkJoin");
TaskQuery query = taskService.createTaskQuery()
.processInstanceId(pi.getId())
.orderByTaskName()
.asc();
List<Task> tasks = query.list();
assertEquals(1, tasks.size());
Task task = tasks.get(0);
assertEquals("Ship Order", task.getName());
完成此任务后,第二个包含网关将加入两个执行,并且由于只有一个传出顺序流,因此不会创建并发执行路径,并且只有存档顺序任务将处于活动状态。
注意,不需要平衡包含网关(对应的包含网关的传入/传出序列流的匹配数量)。包容性网关将简单地等待所有传入的序列流,并为每个传出序列流创建并发执行路径,而不受流程模型中其他构造的影响。
1.3.4 基于事件的网关
描述
基于事件的网关(event-based gateway)提供了根据事件做选择的方式。网关的每一条出口顺序流都需要连接至一个捕获中间事件。当流程执行到达基于事件的网关时,与等待状态类似,网关会暂停执行,并且为每一条出口顺序流创建一个事件订阅。
请注意:基于事件的网关的出口顺序流与一般的顺序流不同。这些顺序流从不实际执行。相反,它们用于告知流程引擎:当执行到达一个基于事件的网关时,需要订阅什么事件。有以下限制:
- 一个基于事件的网关,必须有两条或更多的出口顺序流。
- 基于事件的网关,只能连接至
intermediateCatchEvent(捕获中间事件)
类型的元素(Flowable不支持在基于事件的网关之后连接“接收任务 Receive Task”)。 - 连接至基于事件的网关的
intermediateCatchEvent
,必须只有一个入口顺序流。
图形符号
基于事件的网关,用内部带有特殊图标的网关(菱形)表示。
XML表示
用于定义基于事件的网关的XML元素是eventBasedGateway
。
例子)
下面是一个带有基于事件的网关的示例流程。当执行到达基于事件的网关时,流程执行暂停。流程实例订阅alert信号事件,并创建一个10分钟后触发的定时器。流程引擎会等待10分钟,并同时等待信号事件。如果信号在10分钟内触发,则会取消定时器,流程沿着信号继续执行,激活Handle alert用户任务。如果10分钟内没有触发信号,则会继续执行,并取消信号订阅。
<definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples">
<signal id="alertSignal" name="alert" />
<process id="catchSignal">
<startEvent id="start" />
<sequenceFlow sourceRef="start" targetRef="gw1" />
<eventBasedGateway id="gw1" />
<sequenceFlow sourceRef="gw1" targetRef="signalEvent" />
<sequenceFlow sourceRef="gw1" targetRef="timerEvent" />
<intermediateCatchEvent id="signalEvent" name="Alert">
<signalEventDefinition signalRef="alertSignal" />
</intermediateCatchEvent>
<intermediateCatchEvent id="timerEvent" name="Alert">
<timerEventDefinition>
<timeDuration>PT10M</timeDuration>
</timerEventDefinition>
</intermediateCatchEvent>
<sequenceFlow sourceRef="timerEvent" targetRef="exGw1" />
<sequenceFlow sourceRef="signalEvent" targetRef="task" />
<userTask id="task" name="Handle alert"/>
<exclusiveGateway id="exGw1" />
<sequenceFlow sourceRef="task" targetRef="exGw1" />
<sequenceFlow sourceRef="exGw1" targetRef="end" />
<endEvent id="end" />
</process>
</definitions>
1.4 任务
1.4.1 用户任务
描述
一个用户任务被用来模拟需要由人来完成的工作。当流程执行到达此类用户任务时,将在分配给该任务的任何用户或组的任务列表中创建新任务。
图形符号
用户任务可视化为典型任务(圆角矩形),左上角有一个小用户图标。
XML表示
用户任务在XML中定义如下。该ID是必需的属性,在名称属性是可选的。
<userTask id="theTask" name="Important task" />
用户任务也可以具有描述。实际上,任何BPMN 2.0元素都可以有描述。通过添加文档元素来定义描述。
<userTask id="theTask" name="Schedule meeting" >
<documentation>
Schedule an engineering meeting for next week with the new hire.
</documentation>
可以使用标准Java方式从任务中检索描述文本:
task.getDescription()
截止日期
每个任务都可以使用一个字段标志该任务的到期日期(due date)。可以使用查询API,查询在给定日期前或后到期的任务。
可以在任务定义中使用扩展指定表达式,以在任务创建时设定到期日期。该表达式必须解析为java.util.Date,java.util.String (ISO8601格式),ISO8601时间长度(例如PT50M),或者null。例如,可以使用在流程里前一个表单中输入的日期,或者由前一个服务任务计算出的日期。如果使用的是时间长度,则到期日期基于当前时间加上给定长度计算。例如当dueDate使用“PT30M”时,任务在从现在起30分钟后到期。
<userTask id="theTask" name="Important task" flowable:dueDate="${dateVariable}"/>
任务的截止日期也可以使用传递的TaskService
或TaskListener
使用传递来更改DelegateTask
。
用户分配
用户任务可以直接分配给用户。这是通过定义humanPerformer子元素来完成的。这样的humanPerformer定义需要一个实际定义用户的resourceAssignmentExpression。目前,仅支持formalExpressions。
<process >
...
<userTask id='theTask' name='important task' >
<humanPerformer>
<resourceAssignmentExpression>
<formalExpression>kermit</formalExpression>
</resourceAssignmentExpression>
</humanPerformer>
</userTask>
只能指定一个用户作为任务的humanPerformer。在Flowable术语中,这个用户被称作办理人(assignee)。拥有办理人的任务,在其他人的任务列表中不可见,而只能在该办理人的个人任务列表中看到。
可以通过TaskService获取特定用户办理的任务:
1List<Task> tasks = taskService.createTaskQuery().taskAssignee("kermit").list();
任务也可以放在用户的候选任务列表中。在这个情况下,需要使用**potentialOwner(潜在用户)**结构。用法与humanPerformer结构类似。请注意需要指定表达式中的每一个元素为用户还是组(引擎无法自行判断)。
<process >
...
<userTask id='theTask' name='important task' >
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>user(kermit), group(management)</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>
使用潜在所有者构造定义的任务可以按如下方式检索(或与受让人的任务类似的TaskQuery用法):
List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit").list();
这将检索kermit是候选用户的所有任务,换句话说,正式表达式包含user(kermit)。这还将检索分配给kermit所属的组的所有任务(例如,组(管理),如果kermit是该组的成员并且使用了Flowable标识组件)。用户的组在运行时解析,可以通过IdentityService进行管理。
如果没有给出关于给定文本字符串是用户还是组的具体信息,则引擎默认为分组。以下内容与*宣布组(会计)*时相同。
<formalExpression>accountancy</formalExpression>
用于任务指派的Flowable扩展
很明显,当指派关系不复杂时,这种用户与组的指派方式十分笨重。为避免这种复杂性,可以在用户任务上使用自定义扩展。
- assignee(办理人)属性:这个自定义扩展用于直接将用户指派至用户任务。
1<userTask id="theTask" name="my task" flowable:assignee="kermit" />
与上面定义的humanPerformer结构效果完全相同。
- candidateUsers(候选用户)属性:这个自定义扩展用于为任务指定候选用户。
<userTask id="theTask" name="my task" flowable:candidateUsers="kermit, gonzo" />
与使用上面定义的potentialOwner结构效果完全相同。请注意不需要像在potentialOwner中一样,使用*user(kermit)*的声明,因为这个属性只能用于用户。
- candidateGroups(候选组)attribute:这个自定义扩展用于为任务指定候选组。
<userTask id="theTask" name="my task" flowable:candidateGroups="management, accountancy" />
与使用上面定义的potentialOwner结构效果完全相同。请注意不需要像在potentialOwner中一样,使用*group(management)*的声明,因为这个属性只能用于组。
- 可以定义在一个用户任务上同时定义candidateUsers与candidateGroups。
请注意:尽管Flowable提供了IdentityService身份管理组件,但并不会检查给定的用户是否实际存在。这是为了便于将Flowable嵌入应用时,与已有的身份管理解决方案进行集成。
自定义身份关联类型
在用户指派中已经介绍过,BPMN标准支持单个指派用户即hunamPerformer,或者由一组用户构成potentialOwners潜在用户池。另外,Flowable为用户任务定义了扩展属性元素,用于代表任务的办理人或者候选用户。
Flowable支持的身份关联(identity link)类型有:
public class IdentityLinkType {
/* Flowable native roles */
public static final String ASSIGNEE = "assignee";
public static final String CANDIDATE = "candidate";
public static final String OWNER = "owner";
public static final String STARTER = "starter";
public static final String PARTICIPANT = "participant";
}
BPMN标准及Flowable示例中,身份认证是用户与组。在前一章节提到过,Flowable的身份管理实现并不适用于生产环境,而需要在支持的认证概要下自行扩展。
如果需要添加额外的关联类型,可按照下列语法,使用自定义资源作为扩展元素:
<userTask id="theTask" name="make profit">
<extensionElements>
<flowable:customResource flowable:name="businessAdministrator">
<resourceAssignmentExpression>
<formalExpression>user(kermit), group(management)</formalExpression>
</resourceAssignmentExpression>
</flowable:customResource>
</extensionElements>
</userTask>
自定义关联表达式添加至TaskDefinition类:
protected Map<String, Set<Expression>> customUserIdentityLinkExpressions =
new HashMap<String, Set<Expression>>();
protected Map<String, Set<Expression>> customGroupIdentityLinkExpressions =
new HashMap<String, Set<Expression>>();
public Map<String, Set<Expression>> getCustomUserIdentityLinkExpressions() {
return customUserIdentityLinkExpressions;
}
public void addCustomUserIdentityLinkExpression(
String identityLinkType, Set<Expression> idList) {
customUserIdentityLinkExpressions.put(identityLinkType, idList);
}
public Map<String, Set<Expression>> getCustomGroupIdentityLinkExpressions() {
return customGroupIdentityLinkExpressions;
}
public void addCustomGroupIdentityLinkExpression(
String identityLinkType, Set<Expression> idList) {
customGroupIdentityLinkExpressions.put(identityLinkType, idList);
}
这些方法将在运行时,由UserTaskActivityBehavior的handleAssignments方法调用。
最后,需要扩展IdentityLinkType类,以支持自定义身份关联类型:
package com.yourco.engine.task;
public class IdentityLinkType extends org.flowable.engine.task.IdentityLinkType {
public static final String ADMINISTRATOR = "administrator";
public static final String EXCLUDED_OWNER = "excludedOwner";
}
通过任务监听器进行自定义分配
如果以前的方法不够,可以使用create事件上的任务侦听器委托自定义分配逻辑:
<userTask id="task1" name="My task" >
<extensionElements>
<flowable:taskListener event="create" class="org.flowable.MyAssignmentHandler" />
</extensionElements>
</userTask>
将DelegateTask
传递到TaskListener
执行可以设置受让人和候选用户/组:
public class MyAssignmentHandler implements TaskListener {
public void notify(DelegateTask delegateTask) {
// Execute custom identity lookups here
// and then for example call following methods:
delegateTask.setAssignee("kermit");
delegateTask.addCandidateUser("fozzie");
delegateTask.addCandidateGroup("management");
...
}
}
当使用Spring时,可以按上面章节的介绍使用自定义指派属性,并交由使用任务监听器、带有表达式的Spring bean,监听任务创建事件。在下面的例子中,通过调用ldapService
Spring bean的findManagerOfEmployee
方法设置办理人。传递的emp参数是一个流程变量。
<userTask id="task" name="My Task" flowable:assignee="${ldapService.findManagerForEmployee(emp)}"/>
这也适用于候选用户和组:
<userTask id="task" name="My Task" flowable:candidateUsers="${ldapService.findAllSales()}"/>
请注意,这仅在调用方法的返回类型为String
或Collection<String>
(对于候选用户和组)时才有效:
public class FakeLdapService {
public String findManagerForEmployee(String employee) {
return "Kermit The Frog";
}
public List<String> findAllSales() {
return Arrays.asList("kermit", "gonzo", "fozzie");
}
}
1.4.2 脚本任务
描述
脚本任务是一种自动活动。当进程执行到达脚本任务时,将执行相应的脚本。
属性:
- name:任务属性,用于指示任务的名称
- type:任务属性,其值必须为“script”以指示任务的类型
- scriptFormat:指示脚本语言的扩展属性(例如,javascript,groovy)
- script:要执行的脚本,定义为名为“script”的字段元素中的字符串
- autoStoreVariables:可选的任务属性标志(默认值:false),指示脚本中定义的变量是否将存储在执行上下文中(请参阅下面的注释)
- resultVariableName:可选的任务属性,当存在时,将在Execution上下文中使用脚本评估结果存储具有指定名称的变量(请参阅下面的注释)
图形符号
脚本任务可视化为典型的BPMN 2.0任务(圆角矩形),矩形的左上角有一个小脚本图标。
XML表示
通过指定脚本和scriptFormat来定义脚本任务。
<scriptTask id="theScriptTask" name="Execute script" scriptFormat="groovy">
<script>
sum = 0
for ( i in inputArray ) {
sum += i
}
</script>
</scriptTask>
scriptFormat属性的值,必须是兼容JSR-223(Java平台脚本)的名字。默认情况下,JavaScript包含在每一个JDK中,因此不需要添加任何JAR文件。如果想使用其它(兼容JSR-223的)脚本引擎,则需要在classpath中添加相应的jar,并使用适当的名字。例如,Flowable单元测试经常使用Groovy,因为其语法与Java十分相似。
请注意Groovy脚本引擎与groovy-all JAR捆绑在一起。在Groovy 2.0版本以前,脚本引擎是Groovy JAR的一部分。因此,必须添加如下依赖:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-jsr223</artifactId>
<version>2.x.x<version>
</dependency>
脚本中的变量
可以在脚本中使用通过脚本任务中到达的执行可访问的所有过程变量。在该示例中,脚本变量*“inputArray”*实际上是一个过程变量(整数数组)。
<script>
sum = 0
for ( i in inputArray ) {
sum += i
}
</script>
也可以简单地调用execution.setVariable("variableName", variableValue),在脚本中设置流程变量。默认情况下,变量不会自动储存(请注意,在一些早期版本中是会储存的!)。可以将scriptTask
的autoStoreVariables
参数设置为true
,以自动保存任何在脚本中定义的变量(例如上例中的sum)。然而这并不是最佳实践。最佳实践是显式调用execution.setVariable(),因为在JDK近期的一些版本中,某些脚本语言不能自动保存变量。查看这个链接了解更多信息。
<scriptTask id="script" scriptFormat="JavaScript" flowable:autoStoreVariables="false">
此参数的缺省值是false
,这意味着如果从脚本任务定义中省略该参数,则所有声明的变量将仅在脚本的持续时间内存在。
以下是如何在脚本中设置变量的示例:
<script>
def scriptVar = "test123"
execution.setVariable("myVar", scriptVar)
</script>
注意:以下名称是保留的,不能用作变量名:out,out:print,lang:import,context,elcontext。
脚本任务的结果
脚本任务的返回值,可以通过为脚本任务定义的*'flowable:resultVariable'*属性设置为流程变量。可以是已经存在的,或者新的流程变量。如果指定为已存在的流程变量,则流程变量的值会被脚本执行的结果值覆盖。如果不指定结果变量名,则脚本结果值将被忽略。
<scriptTask id="theScriptTask" name="Execute script" scriptFormat="juel" flowable:resultVariable="myVar">
<script>#{echo}</script>
</scriptTask>
在上面的示例中,脚本执行的结果(已解析表达式*'#{echo}'的值*)在脚本完成后设置为名为*'myVar'*的流程变量。
安全
当使用javascript作为脚本语言来使用安全脚本时也是可能的。请参阅安全脚本部分。
1.4.3 Java服务任务
描述
Java服务任务用于调用外部Java类。
图形符号
服务任务可视化为圆角矩形,左上角有一个小齿轮图标。
XML表示
有四种方法声明如何调用Java逻辑:
- 指定实现了JavaDelegate或ActivityBehavior的类
- 调用解析为委托对象(delegation object)的表达式
- 调用方法表达式(method expression)
- 对值表达式(value expression)求值
要指定在流程执行期间调用的类,需要由flowable:class属性提供完全限定的类名。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:class="org.flowable.MyJavaDelegate" />
有关如何使用此类的更多详细信息,请参阅实现部分。
也可以使用解析为对象的表达式。此对象必须遵循与flowable:class
使用该属性时创建的对象相同的规则(请参阅参考资料)。
<serviceTask id="serviceTask" flowable:delegateExpression="${delegateExpressionBean}" />
这里delegateExpressionBean
是一个实现JavaDelegate
接口的bean,在例如Spring容器中定义。
要指定应评估的UEL方法表达式,请使用属性flowable:expression。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage()}" />
printMessage
将在名为的命名对象上调用方法(不带参数)printer
。
也可以使用表达式中使用的方法传递参数。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage(execution, myVar)}" />
printMessage
将在名为的对象上调用方法printer
。传递的第一个参数是DelegateExecution
,默认情况下可用于表达式上下文中,可用作execution
。传递的第二个参数是myVar
当前执行中具有name的变量的值。
要指定应评估的UEL值表达式,请使用属性flowable:expression。
<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{split.ready}" />
财产的getter方法ready
,getReady
(不带参数),将在叫的bean被调用split
。命名对象在执行的过程变量中解析,并在Spring上下文中(如果适用)。
实现
要实现可在流程执行期间调用的类,该类需要实现org.flowable.engine.delegate.JavaDelegate接口并在execute方法中提供所需的逻辑。当流程执行到达此特定步骤时,它将执行该方法中定义的逻辑,并以默认的BPMN 2.0方式离开活动。
例如,让我们创建一个Java类,可用于将流程变量String更改为大写。这个类需要实现org.flowable.engine.delegate.JavaDelegate接口,这需要我们实现*execute(DelegateExecution)*方法。这个操作将由引擎调用,并且需要包含业务逻辑。可以通过DelegateExecution接口访问和操作流程实例信息(如流程变量)(单击链接以获取其操作的详细Javadoc)。
public class ToUppercase implements JavaDelegate {
public void execute(DelegateExecution execution) {
String var = (String) execution.getVariable("input");
var = var.toUpperCase();
execution.setVariable("input", var);
}
}
请注意:只会为serviceTask上定义的Java类创建一个实例。所有流程实例共享同一个类实例,用于调用execute(DelegateExecution)。这意味着该类不能有任何成员变量,并需要是线程安全的,因为它可能会在不同线程中同时执行。这也影响了字段注入的使用方法。(译者注:原文可能较老,不正确。5.21中,flowable:class指定的类,会在流程实例启动时,为每个活动分别进行实例化。不过,当该活动在流程中重复执行,或者为多实例时,使用的都会是同一个类实例。)
在流程定义中引用(如flowable:class
)的类,不会在部署时实例化。只有当流程执行第一次到达该类使用的地方时,才会创建该类的实例。如果找不到这个类,会抛出FlowableException
。这是因为部署时的环境(更准确的说classpath),与实际运行的环境经常不一样。例如当使用ant或者Flowable应用中业务存档上传的方式部署的流程,其classpath中不会自动包含流程引用的类。
[内部:非公开实现类]也可以使用实现了org.flowable.engine.impl.delegate.ActivityBehavior接口的类。该实现可以访问更强大的引擎功能,例如,可以影响流程的控制流程。请注意这并不是很好的实践,需要避免这么使用。建议只有在高级使用场景下,并且你确知在做什么的时候,才使用ActivityBehavior接口。
字段注入
可以将值注入委托类的字段中。支持以下类型的注射:
- 固定字符串值
- 表达式
如果可以的话,会按照Java Bean命名约定(例如,firstName
成员使用setter setFirstName(…)
),通过委托类的公有setter方法,注入变量。如果该字段没有可用的setter,会直接设置该委托类的私有成员的值。有的环境中,SecurityManagers不允许修改私有字段,因此为想要注入的字段暴露一个公有setter方法,是更安全的做法。
不论在流程定义中声明的是什么类型的值,注入对象的setter/私有字段的类型,总是org.flowable.engine.delegate.Expression。解析表达式后,可以被转型为合适的类型。
'flowable:class'属性支持字段注入。也可以在使用flowable:delegateExpression属性时,进行字段注入。然而考虑到线程安全,需要遵循特殊的规则(参见下一章节)。
下面的代码片段展示了如何为类中声明的字段注入常量值。请注意按照BPMN 2.0 XML概要的要求,在实际字段注入声明前,需要先声明’extensionElements’XML元素。
<serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
<extensionElements>
<flowable:field name="text" stringValue="Hello World" />
</extensionElements>
</serviceTask>
该类ToUpperCaseFieldInjected
有一个text
类型的字段org.flowable.engine.delegate.Expression
。调用时text.getValue(execution)
,Hello World
将返回配置的字符串值:
public class ToUpperCaseFieldInjected implements JavaDelegate {
private Expression text;
public void execute(DelegateExecution execution) {
execution.setVariable("var", ((String)text.getValue(execution)).toUpperCase());
}
}
或者,对于长文本(例如,内联电子邮件),可以使用*'flowable:string'*子元素:
<serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
<extensionElements>
<flowable:field name="text">
<flowable:string>
This is a long string with a lot of words and potentially way longer even!
</flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
可以使用表达式在运行时动态解析注入的值。这种表达式可以使用流程变量,或者(若使用Spring)Spring定义的Bean。在服务任务实现中提到过,当服务任务中使用flowable:class属性时,该Java类的实例在所有流程实例中共享。要动态地为字段注入值,可以在org.flowable.engine.delegate.Expression
中注入值或方法表达式,它们会通过execute
方法传递的DelegateExecution
计算/调用。
下面的示例类使用了注入的表达式,并使用当前的DelegateExecution
解析它们。调用genderBean方法时传递的是gender变量。完整的代码与测试可以在org.flowable.examples.bpmn.servicetask.JavaServiceTaskTest.testExpressionFieldInjection
中找到
<serviceTask id="javaService" name="Java service invocation"
flowable:class="org.flowable.examples.bpmn.servicetask.ReverseStringsFieldInjected">
<extensionElements>
<flowable:field name="text1">
<flowable:expression>${genderBean.getGenderString(gender)}</flowable:expression>
</flowable:field>
<flowable:field name="text2">
<flowable:expression>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</flowable:expression>
</flowable:field>
</ extensionElements>
</ serviceTask>
public class ReverseStringsFieldInjected implements JavaDelegate {
private Expression text1;
private Expression text2;
public void execute(DelegateExecution execution) {
String value1 = (String) text1.getValue(execution);
execution.setVariable("var1", new StringBuffer(value1).reverse().toString());
String value2 = (String) text2.getValue(execution);
execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
}
}
或者,您也可以将表达式设置为属性而不是子元素,以使XML更简洁。
<flowable:field name="text1" expression="${genderBean.getGenderString(gender)}" />
<flowable:field name="text1" expression="Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}" />
字段注入与线程安全
通常情况下,在服务任务中使用Java委托与字段注入是线程安全的。然而,有些情况下不能保证线程安全。这取决于设置,或Flowable运行的环境。
当使用flowable:class属性时,使用字段注入总是线程安全的(译者注:仍不完全安全,如对于多实例服务任务,使用的是同一个实例)。对于引用了某个类的每一个服务任务,都会实例化新的实例,并且在创建实例时注入一次字段。在不同的任务或流程定义中多次使用同一个类没有问题。
当使用flowable:expression属性时,不能使用字段注入。只能通过方法调用传递变量。总是线程安全的。
当使用flowable:delegateExpression属性时,委托实例的线程安全性,取决于表达式解析的方式。如果该委托表达式在多个任务或流程定义中重复使用,并且表达式总是返回相同的示例,则字段注入不是线程安全的。让我们看几个例子。
假设表达式为*${factory.createDelegate(someVariable)}*,其中factory为引擎可用的Java bean(例如使用Spring集成时的Spring bean),并在每次表达式解析时创建新的实例。这种情况下,使用字段注入时,没有线程安全性问题:每次表达式解析时,都会注入新实例的字段。
然而,如果表达式为*${someJavaDelegateBean}*,解析为JavaDelegate的实现,并且在创建单例的环境(如Spring)中运行。当在不同的任务或流程定义中使用这个表达式时,表达式总会解析为相同的实例。这种情况下,使用字段注入不是线程安全的。例如:
<serviceTask id="serviceTask1" flowable:delegateExpression="${someJavaDelegateBean}">
<extensionElements>
<flowable:field name="someField" expression="${input * 2}"/>
</extensionElements>
</serviceTask>
<!-- other process definition elements -->
<serviceTask id="serviceTask2" flowable:delegateExpression="${someJavaDelegateBean}">
<extensionElements>
<flowable:field name="someField" expression="${input * 2000}"/>
</extensionElements>
</serviceTask>
这段示例代码有两个服务任务,使用同一个委托表达式,但是expression字段填写不同的值。如果该表达式解析为相同的实例,就会在并发场景下,注入someField字段时出现竞争条件。
最简单的解决方法是:
- 使用表达式代替直接使用Java委托,并将所需数据通过方法参数传递给委托。
- 或者,在每次委托表达式解析时,返回委托类的新实例。这意味着这个bean的scope必须是prototype(原型)(例如在委托类上加上@Scope(SCOPE_PROTOTYPE)注解)。
在Flowable 5.22版本中,可以通过配置流程引擎配置,禁用在委托表达式上使用字段注入。需要设置delegateExpressionFieldInjectionMode参数(取org.flowable.engine.imp.cfg.DelegateExpressionFieldInjectionMode枚举中的值)。
可使用下列选项:
- DISABLED(禁用):当使用委托表达式时,完全禁用字段注入。不会再尝试进行字段注入。这是最安全的方式,保证线程安全。
- COMPATIBILITY(兼容):在这个模式下,行为与V5.21之前完全一样:可以在委托表达式中使用字段注入,如果委托类中没有定义该字段,会抛出异常。这是最不线程安全的模式,但可以保证历史版本兼容性,也可以在委托表达式只在一个任务中使用的时候(因此不会产生并发竞争条件),安全使用。
- MIXED(混合):可以在使用委托表达式时注入,但当委托中没有定义字段时,不会抛出异常。这样可以在部分委托(比如不是单例的实例)中使用注入,而在部分委托中不使用注入。
- Flowable 5.x版本的默认模式为COMPATIBILITY。
- Flowable 6.x版本的默认模式为MIXED。
例如,假设使用MIXED模式,并使用Spring集成,在Spring配置中定义了如下bean:
<bean id="singletonDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.SingletonDelegateExpressionBean" />
<bean id="prototypeDelegateExpressionBean"
class="org.flowable.spring.test.fieldinjection.PrototypeDelegateExpressionBean"
scope="prototype" />
第一个bean是一个普通的Spring bean,因此是一个单例。第二个原型作为作用域,每次请求bean时,Spring容器都会返回一个新实例。
给定以下流程定义:
<serviceTask id="serviceTask1" flowable:delegateExpression="${prototypeDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${input * 2}"/>
<flowable:field name="fieldB" expression="${1 + 1}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask1"/>
</extensionElements>
</serviceTask>
<serviceTask id="serviceTask2" flowable:delegateExpression="${prototypeDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${123}"/>
<flowable:field name="fieldB" expression="${456}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask2"/>
</extensionElements>
</serviceTask>
<serviceTask id="serviceTask3" flowable:delegateExpression="${singletonDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${input * 2}"/>
<flowable:field name="fieldB" expression="${1 + 1}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask1"/>
</extensionElements>
</serviceTask>
<serviceTask id="serviceTask4" flowable:delegateExpression="${singletonDelegateExpressionBean}">
<extensionElements>
<flowable:field name="fieldA" expression="${123}"/>
<flowable:field name="fieldB" expression="${456}"/>
<flowable:field name="resultVariableName" stringValue="resultServiceTask2"/>
</extensionElements>
</serviceTask>
我们有四个服务任务,第一个和第二个使用*$ {prototypeDelegateExpressionBean}委托表达式,第三个和第四个使用$ {singletonDelegateExpressionBean}*委托表达式。
让我们先看看原型bean:
public class PrototypeDelegateExpressionBean implements JavaDelegate {
public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);
private Expression fieldA;
private Expression fieldB;
private Expression resultVariableName;
public PrototypeDelegateExpressionBean() {
INSTANCE_COUNT.incrementAndGet();
}
@Override
public void execute(DelegateExecution execution) {
Number fieldAValue = (Number) fieldA.getValue(execution);
Number fieldValueB = (Number) fieldB.getValue(execution);
int result = fieldAValue.intValue() + fieldValueB.intValue();
execution.setVariable(resultVariableName.getValue(execution).toString(), result);
}
}
当我们在运行上面的流程定义的流程实例之后检查INSTANCE_COUNT时,我们将返回两个,因为每次解析*$ {prototypeDelegateExpressionBean}时都会创建一个新实例。可以在这里注入字段而没有任何问题,我们可以在这里看到三个表达式*成员字段。
然而,单例bean看起来略有不同:
public class SingletonDelegateExpressionBean implements JavaDelegate {
public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);
public SingletonDelegateExpressionBean() {
INSTANCE_COUNT.incrementAndGet();
}
@Override
public void execute(DelegateExecution execution) {
Expression fieldAExpression = DelegateHelper.getFieldExpression(execution, "fieldA");
Number fieldA = (Number) fieldAExpression.getValue(execution);
Expression fieldBExpression = DelegateHelper.getFieldExpression(execution, "fieldB");
Number fieldB = (Number) fieldBExpression.getValue(execution);
int result = fieldA.intValue() + fieldB.intValue();
String resultVariableName = DelegateHelper.getFieldExpression(execution,
"resultVariableName").getValue(execution).toString();
execution.setVariable(resultVariableName, result);
}
}
在对于单例bean,INSTANCE_COUNT总是1。在这个委托中,没有Expression成员字段(使用MIXED模式)。而在COMPATIBILITY模式下,就会抛出异常,因为需要有成员字段。这个bean也可以使用DISABLED模式,但会禁用上面进行了字段注入的原型bean。
在委托的代码里,使用了org.flowable.engine.delegate.DelegateHelper。它提供了一些有用的工具方法,用于执行相同的逻辑,并且在单例中是线程安全的。与注入Expression不同,它通过getFieldExpression读取。这意味着在服务任务的XML里,字段定义与单例bean完全相同。查看上面的XML代码,可以看到定义是相同的,只是实现逻辑不同。
技术提示:getFieldExpression直接读取BpmnModel,并在方法执行时创建表达式,因此是线程安全的。
- 在Flowable V5.x版本中,(由于架构缺陷)不能在ExecutionListener或TaskListener中使用DelegateHelper。要保证监听器的线程安全,仍需使用表达式,或确保每次解析委托表达式时,都创建新实例。
- 在Flowable V6.x版本中,在ExecutionListener或TaskListener中可以使用DelegateHelper。例如在V6.x版本中,下列代码可以使用DelegateHelper:
<extensionElements>
<flowable:executionListener
delegateExpression="${testExecutionListener}" event="start">
<flowable:field name="input" expression="${startValue}" />
<flowable:field name="resultVar" stringValue="processStartValue" />
</flowable:executionListener>
</extensionElements>
其中testExecutionListener解析为ExecutionListener接口的一个实现的实例:
@Component("testExecutionListener")
public class TestExecutionListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) {
Expression inputExpression = DelegateHelper.getFieldExpression(execution, "input");
Number input = (Number) inputExpression.getValue(execution);
int result = input.intValue() * 100;
Expression resultVarExpression = DelegateHelper.getFieldExpression(execution, "resultVar");
execution.setVariable(resultVarExpression.getValue(execution).toString(), result);
}
}
服务任务结果
服务执行的返回值(仅对使用表达式的服务任务),可以通过为服务任务定义的*'flowable:resultVariable'属性设置为流程变量。可以是已经存在的,或者新的流程变量。 如果指定为已存在的流程变量,则流程变量的值会被服务执行的结果值覆盖。 如果使用'flowable:useLocalScopeForResultVariable'*,则会将结果值设置为局部变量。 如果不指定结果变量名,则服务任务的结果值将被忽略。
<serviceTask id="aMethodExpressionServiceTask"
flowable:expression="#{myService.doSomething()}"
flowable:resultVariable="myVar" />
在上例中,服务执行的结果(流程变量或Spring bean中,使用*'myService'名字所获取的对象,调用'doSomething()'方法的返回值),在服务执行完成后,会设置为名为'myVar'*的流程变量。
可触发
一种常见的模式是发送JMS消息或HTTP调用触发外部服务,然后流程实例进入等待状态。之后外部系统会回复响应,流程实例继续执行下一个活动。在默认的BPMN中,需要使用服务任务和接收任务(receive task)。但是这样会引入竞争条件:外部服务的响应可能会早于流程实例持久化及接收任务激活。为了解决这个问题,Flowable为服务任务增加了triggerable(可触发)属性,可以将服务任务转变为执行服务逻辑,并在继续执行之前等待外部触发的任务。如果在可触发服务任务上同时设置异步(async 为 true),则流程实例会先持久化,然后在异步作业中执行服务任务逻辑。在BPMN XML中,可以这样实现可触发服务任务:
<serviceTask id="aTriggerableServiceTask"
flowable:expression="#{myService.doSomething()}"
flowable:triggerable="true"
flowable:async="true" />
外部服务可以同步或异步地触发等待中的流程实例。为了避免乐观锁异常,最好使用异步触发。默认情况下,异步作业是排他的,也就是说流程实例会被锁定,以保证流程实例中的其他活动不会影响到触发器的逻辑。可以使用RuntimeService的triggerAsync方法,异步触发等待中的流程实例。当然还是可以使用RuntimeService的trigger方法,同步触发。
处理异常
当执行自定义逻辑时,通常需要捕获并在流程中处理特定的业务异常。Flowable提供了多种选择。
抛出BPMN错误
可以在服务任务或脚本任务的用户代码中抛出BPMN错误。可以在Java委托、脚本、表达式与委托表达式中,抛出特殊的FlowableException:BpmnError。引擎会捕获这个异常,并将其转发至合适的错误处理器,如错误边界事件或错误事件子流程。
public class ThrowBpmnErrorDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
try {
executeBusinessLogic();
} catch (BusinessException e) {
throw new BpmnError("BusinessExceptionOccurred");
}
}
}
构造函数的参数是错误代码。错误代码决定了处理这个错误的错误处理器。参见错误边界事件了解如何捕获BPMN错误。
这个机制只应该用于业务错误,需要通过流程中定义的错误边界事件或错误事件子流程处理。技术错误应该通过其他异常类型表现,并且通常不在流程内部处理。
异常映射
也可以使用mapException
扩展,直接将Java异常映射至业务异常(错误)。单映射是最简单的形式:
<serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException
errorCode="myErrorCode1">org.flowable.SomeException</flowable:mapException>
</extensionElements>
</serviceTask>
在上面的代码中,如果服务任务抛出org.flowable.SomeException
的实例,引擎会捕获该异常,并将其转换为带有给定errorCode的BPMN错误。然后就可以像普通BPMN错误完全一样地处理。其他的异常没有映射,仍将抛出至API调用处。
也可以在单行中使用includeChildExceptions
属性,映射特定异常的所有子异常。
<serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException errorCode="myErrorCode1"
includeChildExceptions="true">org.flowable.SomeException</flowable:mapException>
</extensionElements>
</serviceTask>
上面的代码中,Flowable会将SomeException
的任何直接或间接的子类,转换为带有指定错误代码的BPMN错误。 当未指定includeChildExceptions
时,视为“false”。
默认映射最泛用。默认映射是一个不指定类的映射,可以匹配任何Java异常:
<serviceTask id="servicetask1" name="Service Task" flowable:class="...">
<extensionElements>
<flowable:mapException errorCode="myErrorCode1"/>
</extensionElements>
</serviceTask>
除了默认映射,会按照从上至下的顺序检查映射,使用第一个匹配的映射。只在所有映射都不能成功匹配时使用默认映射。 只有第一个不指定类的映射会作为默认映射。默认映射忽略includeChildExceptions
。
异常顺序流
也可以选择在发生异常时,将流程执行路由至另一条路径。下面是一个例子。
<serviceTask id="javaService"
name="Java service invocation"
flowable:class="org.flowable.ThrowsExceptionBehavior">
</serviceTask>
<sequenceFlow id="no-exception" sourceRef="javaService" targetRef="theEnd" />
<sequenceFlow id="exception" sourceRef="javaService" targetRef="fixException" />
这里,服务任务有两个输出序列流,名为exception
和no-exception
。如果存在异常,序列流ID将用于指导流程流程:
public class ThrowsExceptionBehavior implements ActivityBehavior {
public void execute(DelegateExecution execution) {
String var = (String) execution.getVariable("想`");
String sequenceFlowToTake = null;
try {
executeLogic(var);
sequenceFlowToTake = "no-exception";
} catch (Exception e) {
sequenceFlowToTake = "exception";
}
DelegateHelper.leaveDelegate(execution, sequenceFlowToTake);
}
}
在JavaDelegate中使用Flowable服务
对于某些用例,可能需要在Java服务任务中使用Flowable服务(例如,如果callActivity不适合您的需要,则通过RuntimeService启动流程实例)。
public class StartProcessInstanceTestDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
RuntimeService runtimeService = Context.getProcessEngineConfiguration().getRuntimeService();
runtimeService.startProcessInstanceByKey("myProcess");
}
}
所可以使用这个接口访问所有Flowable服务API。
使用这些API调用造成的所有数据变更都在当前事务中。在依赖注入的环境(如Spring或CDI,无论是否使用启用JTA的数据源)下也可以使用这个接口。例如,下面的代码片段与上面的代码具有相同功能,但通过注入而不是org.flowable.engine.EngineServices接口获得RuntimeService。
@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {
@Autowired
private RuntimeService runtimeService;
public void startProcess() {
runtimeService.startProcessInstanceByKey("oneTaskProcess");
}
}
**重要技术提示:**由于服务调用是在当前事务中完成的,因此在服务任务执行前产生或修改的数据尚未存入数据库。所有API调用都基于数据库数据处理,这意味着这些未提交的修改在服务任务的API调用中“不可见”。
1.4.4 Web服务任务
描述
Web Service任务用于同步调用外部Web服务。
图形符号
Web服务任务的可视化方式与Java服务任务相同。
XML表示
要使用Web服务,我们需要导入其操作和复杂类型。这可以通过使用指向Web服务的WSDL的import标记自动完成:
<import importType="http://schemas.xmlsoap.org/wsdl/"
location="http://localhost:63081/counter?wsdl"
namespace="http://webservice.flowable.org/" />
按照上面的声明,Flowable会导入定义,但不会创建条目定义(item definition)与消息。如果需要调用一个名为’prettyPrint’的方法,则需要先为请求及回复消息创建对应的消息与条目定义:
<message id="prettyPrintCountRequestMessage" itemRef="tns:prettyPrintCountRequestItem" />
<message id="prettyPrintCountResponseMessage" itemRef="tns:prettyPrintCountResponseItem" />
<itemDefinition id="prettyPrintCountRequestItem" structureRef="counter:prettyPrintCount" />
<itemDefinition id="prettyPrintCountResponseItem" structureRef="counter:prettyPrintCountResponse" />
在声明服务任务前,需要定义实际引用Web服务的BPMN接口与操作。基本上,是定义“接口”与所需的“操作”。对每一个操作都可以重复使用之前定义的“传入”与“传出”消息。例如,下面的声明定义了“counter”接口及“prettyPrintCountOperation”操作:
<interface name="Counter Interface" implementationRef="counter:Counter">
<operation id="prettyPrintCountOperation" name="prettyPrintCount Operation"
implementationRef="counter:prettyPrintCount">
<inMessageRef>tns:prettyPrintCountRequestMessage</inMessageRef>
<outMessageRef>tns:prettyPrintCountResponseMessage</outMessageRef>
</operation>
</interface>
然后,我们可以使用## WebService实现和对Web服务操作的引用来声明Web服务任务。
<serviceTask id="webService"
name="Web service invocation"
implementation="##WebService"
operationRef="tns:prettyPrintCountOperation">
Web服务任务IO规范
除非使用简化方法处理输入与输出数据关联(见下),否则需要为每个Web服务任务声明IO规范,指出任务的输入与输出是什么。这个方法很简单,也兼容BPMN 2.0。在prettyPrint例子中,根据之前声明的条目定义,定义输入与输出:
<ioSpecification>
<dataInput itemSubjectRef="tns:prettyPrintCountRequestItem" id="dataInputOfServiceTask" />
<dataOutput itemSubjectRef="tns:prettyPrintCountResponseItem" id="dataOutputOfServiceTask" />
<inputSet>
<dataInputRefs>dataInputOfServiceTask</dataInputRefs>
</inputSet>
<outputSet>
<dataOutputRefs>dataOutputOfServiceTask</dataOutputRefs>
</outputSet>
</ioSpecification>
Web服务任务数据输入关联
有两种指定数据输入关联的方式:
- 使用表达式
- 使用简化方法
使用表达式指定数据输入关联,需要定义源及目标条目,并指定每个条目与字段的关联。下面的例子中,我们针对每个条目,指定prefix与suffix字段:
<dataInputAssociation>
<sourceRef>dataInputOfProcess</sourceRef>
<targetRef>dataInputOfServiceTask</targetRef>
<assignment>
<from>${dataInputOfProcess.prefix}</from>
<to>${dataInputOfServiceTask.prefix}</to>
</assignment>
<assignment>
<from>${dataInputOfProcess.suffix}</from>
<to>${dataInputOfServiceTask.suffix}</to>
</assignment>
</dataInputAssociation>
也可以使用更简单明了的简化方法。'sourceRef’元素是一个Flowable变量名,'targetRef’是条目定义的参数。在下面的例子里,将’PrefixVariable’变量的值关联至’prefix’字段,并将’SuffixVariable’变量的值关联至’suffix’字段。
<dataInputAssociation>
<sourceRef>PrefixVariable</sourceRef>
<targetRef>prefix</targetRef>
</dataInputAssociation>
<dataInputAssociation>
<sourceRef>SuffixVariable</sourceRef>
<targetRef>suffix</targetRef>
</dataInputAssociation>
Web服务任务数据输出关联
有两种方法可以指定数据输出关联:
- 使用表达式
- 使用简单的方法
使用表达式指定数据输出关联,需要定义目标变量及源表达式。这种方法很简单,与数据输入关联类似:
<dataOutputAssociation>
<targetRef>dataOutputOfProcess</targetRef>
<transformation>${dataOutputOfServiceTask.prettyPrint}</transformation>
</dataOutputAssociation>
也可以使用更简单明了的简化方法。'sourceRef’是条目定义的参数,'targetRef’元素是Flowable变量名。这种方法很简单,与数据输入关联类似:
<dataOutputAssociation>
<sourceRef>prettyPrint</sourceRef>
<targetRef>OutputVariable</targetRef>
</dataOutputAssociation>
1.4.5 业务规则任务
描述
业务规则任务(business rule task)用于同步地执行一条或多条规则。Flowable使用名为Drools Expert的Drools规则引擎执行业务规则。目前,业务规则中包含的.drl文件,必须与定义了业务规则服务并执行规则的流程定义一起部署。这意味着流程中使用的所有.drl文件都需要打包在流程BAR文件中,与任务表单等类似。要了解如何为Drools Expert创建业务规则,请访问位于JBoss Drools的Drools文档。
如果想要使用自己的规则任务实现,比如希望通过不同方法使用Drools,或者想使用完全不同的规则引擎,则可以使用BusinessRuleTask的class或expression属性。这样它会与服务任务的行为完全相同。
图示
业务规则任务显示为带有表格图标的圆角矩形
XML表示
要执行与流程定义在同一个BAR文件中部署的一条或多条业务规则,需要定义输入与结果变量。输入变量可以用流程变量的列表定义,使用逗号分隔。输出变量只能有一个变量名,将执行业务规则后的输出对象存储至流程变量。请注意结果变量会包含对象的List。如果没有指定结果变量名,默认为org.flowable.engine.rules.OUTPUT。
下面的业务规则任务,执行与流程定义一起部署的所有业务规则:
<process id="simpleBusinessRuleProcess">
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="businessRuleTask" />
<businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:resultVariable="rulesOutput" />
<sequenceFlow sourceRef="businessRuleTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
业务规则任务还可以配置为仅从部署的.drl文件中执行一组定义的规则。必须为此指定由逗号分隔的规则名称列表。
<businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:rules="rule1, rule2" />
在这种情况下,只执行rule1和rule2。
也可以定义需要从执行中排除的规则列表。
<businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:rules="rule1, rule2" exclude="true" />
这个例子中,除了rule1与rule2之外,其它所有与流程定义一起部署在同一个BAR文件中的规则都会被执行。
前面提到过,还可以自行指定BusinessRuleTask的实现:
<businessRuleTask id="businessRuleTask" flowable:class="${MyRuleServiceDelegate}" />
现在,BusinessRuleTask的行为与ServiceTask完全相同,但仍然保持BusinessRuleTask图标可视化我们在此处进行业务规则处理。
1.4.6 邮件任务
Flowable让你可以通过自动的邮件服务任务(email task),增强业务流程。可以向一个或多个收信人发送邮件,支持cc,bcc,HTML文本,等等。请注意邮件任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,邮件任务实现为一种特殊的服务任务。
配置邮件服务器
Flowable引擎使用支持SMTP的外部邮件服务器发送邮件。为了发送邮件,引擎需要了解如何连接邮件服务器。可以在flowable.cfg.xml配置文件中设置下面的参数:
属性 | 需要? | 描述 |
---|---|---|
mailServerHost | 没有 | 邮件服务器的主机名(如mail.mycorp.com)。默认为localhost |
mailServerPort | 是的,如果没有在默认端口上 | 邮件服务器的SMTP端口。默认值为25 |
mailServerForceTo | 没有 | 如果设置,它将在发送电子邮件时用作邮件任务中配置的to,cc和/或bcc的替代。 |
mailServerDefaultFrom | 没有 | 电子邮件发件人的默认电子邮件地址,如果用户未提供。默认情况下,这是[email protected] |
mailServerUsername | 若服务器需要 | 部分邮件服务器发信时需要进行认证。默认为空。 |
mailServerPassword | 若服务器需要 | 部分邮件服务器发信时需要进行认证。默认为空。 |
mailServerUseSSL | 若服务器需要 | 部分邮件服务器要求ssl通信。默认设置为false。 |
mailServerUseTLS | 若服务器需要 | 部分邮件服务器要求TLS通信(例如gmail)。默认设置为false。 |
定义邮件任务
邮件任务实现为特殊的服务任务,将服务任务的type定义为*'mail'*进行设置。
<serviceTask id="sendMail" flowable:type="mail">
邮件任务通过字段注入配置。这些参数的值可以使用EL表达式,并将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
to | 是 | 邮件的收信人。可以使用逗号分隔的列表定义多个接收人 |
from | 否 | 邮件的发信人地址。如果不设置,会使用默认配置的地址 |
subject | 否 | 邮件的主题 |
cc | 否 | 邮件的抄送人。可以使用逗号分隔的列表定义多个接收人 |
bcc | 否 | 邮件的密送人。可以使用逗号分隔的列表定义多个接收人 |
charset | 否 | 可以指定邮件的字符集,对许多非英语语言很必要。 |
html | 否 | 邮件的HTML文本 |
text | 否 | 邮件的内容,用于纯文本邮件。对于不支持富文本内容的客户端,可以与html一起使用。邮件客户端可以回退为显式纯文本格式。 |
htmlVar | 否 | 存储邮件HTML内容的流程变量名。与html参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。 |
textVar | 否 | 存储邮件纯文本内容的流程变量名。与text参数的最大区别,是这个参数会在邮件任务发送前,使用其内容进行表达式替换。 |
ignoreException | 否 | 处理邮件失败时,是忽略还是抛出FlowableException。默认设置为false。 |
exceptionVariableName | 否 | 如果设置ignoreException = true,而处理邮件失败时,则使用给定名字的变量保存失败信息 |
示例 usage
下面的XML代码片段是使用邮件任务的示例。
<serviceTask id="sendMail" flowable:type="mail">
<extensionElements>
<flowable:field name="from" stringValue="[email protected]" />
<flowable:field name="to" expression="${recipient}" />
<flowable:field name="subject" expression="Your order ${orderId} has been shipped" />
<flowable:field name="html">
<flowable:expression>
<![CDATA[
<html>
<body>
Hello ${male ? 'Mr.' : 'Mrs.' } ${recipientName},<br/><br/>
As of ${now}, your order has been <b>processed and shipped</b>.<br/><br/>
Kind regards,<br/>
TheCompany.
</body>
</html>
]]>
</flowable:expression>
</flowable:field>
</extensionElements>
</serviceTask>
1.4.7 Http任务
Http任务(Http task)用于发出HTTP请求,增强了Flowable的集成能力。请注意Http任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Http任务实现为一种特殊的服务任务。
配置Http客户端
Flowable使用可配置的Http客户端发出Http请求。如果不进行设置,会使用默认配置。
示例配置:
<bean id="processEngineConfiguration"
class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- http client configurations -->
<property name="httpClientConfig" ref="httpClientConfig"/>
</bean>
<bean id="httpClientConfig" class="org.flowable.engine.cfg.HttpClientConfig">
<property name="connectTimeout" value="5000"/>
<property name="socketTimeout" value="5000"/>
<property name="connectionRequestTimeout" value="5000"/>
<property name="requestRetryLimit" value="5"/>
</bean>
参数 | 必填? | 描述 |
---|---|---|
connectTimeout | 否 | 连接超时时间,以毫秒计。 默认值 5000。 |
socketTimeout | 否 | Socket超时时间,以毫秒计。 默认值 5000。 |
connectionRequestTimeout | 否 | 请求连接超时时间。以毫秒计 默认值 5000。 |
requestRetryLimit | 否 | 请求重试次数(“0”代表不重试) 默认值 3。 |
disableCertVerify | 否 | 禁用SSL证书验证。 默认值 false。 |
定义Http任务
Http任务实现为特殊的服务任务,将服务任务的type定义为*'http'*进行设置。
<serviceTask id="httpGet" flowable:type="http">
可以使用自定义的实现,覆盖Http任务的默认行为。 需要扩展org.flowable.http.HttpActivityBehavior,并覆盖perform()方法。
需要在任务定义中设置httpActivityBehaviorClass字段(默认值为 org.flowable.http.impl.HttpActivityBehaviorImpl)。
当前使用的默认实现HttpActivityBehaviorImpl基于Apache Http Client。尽管Apache Http Client可以使用很多方法自定义,但我们并没有在Http客户端配置中使用全部选项。
参考 Http Client builder 创建自定义客户端。
<serviceTask id="httpGet" flowable:type="http">
<extensionElements>
<flowable:field name="httpActivityBehaviorClass">
<flowable:string>
<![CDATA[org.example.flowable.HttpActivityBehaviorCustomImpl]]>
</flowable:string>
</flowable:field>
</extensionElements>
</sericeTask>
Http任务配置
Http任务通过字段注入配置。所有参数都可以配置为EL表达式,在运行时进行解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
requestMethod | 是 | 请求方法 (GET,POST,PUT,DELETE)。 |
requestUrl | yes | 请求URL (例如 - http://flowable.org)。 |
requestHeaders | 否 | 行分隔的Http请求头。 例如 - Content-Type: application/json Authorization: Basic aGFRlc3Q= |
requestBody | 否 | 请求体 例如 - ${sampleBody} |
requestTimeout | 否 | 请求超时时间。单位为毫秒 (例如 - 5000)。 默认值为“0”,即没有超时。 链接相关的超时设置为配置Http客户端。 |
disallowRedirects | 否 | 是否禁用Http重定向。 默认为false。 (例如 - true)。 |
failStatusCodes | 否 | 逗号分隔的Http状态码,将令请求失败并抛出FlowableException。 例如:400, 404, 500, 503 例如:400, 5XX |
handleStatusCodes | 否 | 逗号分隔的Http状态码,将令任务抛出BpmnError,并可用错误边界事件捕获。 BpmnError的错误码为HTTP。 例如,404状态码会将错误码设置为HTTP404。 仅当disallowRedirects字段设置为true时,3XX状态码才会被抛出。若同一个状态码在handleStatusCodes及failStatusCodes中都有配置,则handleStatusCodes生效。 例如:400, 404, 500, 503 例如:3XX, 4XX, 5XX |
ignoreException | 否 | 是否忽略异常。异常将被捕获,并存储在名为*.errorMessage*的变量中。 |
saveRequestVariables | 否 | 是否保存请求变量。 默认情况下,只会保存将响应相关的变量。 |
saveResponseParameters | 否 | 是否保存全部的响应变量,包括HTTP状态码,响应头等。 默认情况下,只会将响应体保存为变量。 |
resultVariablePrefix | 否 | 执行变量名的前缀。 如果不设置前缀,变量名为*.fieldName*。 例如,对于id为task7的任务,其请求URL将保存为task7.requestUrl。 |
saveResponseParametersTransient | 否 | 若为true,则会将响应体变量(如果设置了保存响应头,状态码,也包括在内)设置为瞬时变量。 |
saveResponseVariableAsJson | 否 | 若为true,则响应体会保存为JSON变量,而非String。如果HTTP服务返回JSON,并且想使用点注记方式使用字段(如myResponse.user.name),这个配置就很有用。 |
httpActivityBehaviorClass | 否 | org.flowable.http.HttpActivityBehavior类的自定义扩展的全限定类名。 |
除了上面提到的字段,使用saveResponseParameters还会在执行成功后设置下列变量。
变量 | 可选? | 描述 |
---|---|---|
responseProtocol | 是 | Http版本。 |
responseReason | 是 | Http响应原因短语。 |
responseStatusCode | 是 | Http响应状态码(例如 - 200)。 |
responseHeaders | 是 | 行分隔的Http响应头。 例如 - Content-Type: application/json Content-Length: 777 |
responseBody | 是 | 字符串形式的响应体,若有。 |
errorMessage | 是 | 被忽略的异常信息,若有。 |
结果变量
请注意上述执行变量名都会使用resultVariablePrefix前缀。 例如,可以在其他活动中,使用task7.responseStatusCode获取响应状态码。 其中task7是服务任务的id。可以设置resultVariablePrefix覆盖这个行为。
用法示例
以下XML片段显示了使用Http Task的示例。
<serviceTask id="httpGet" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod" stringValue="GET" />
<flowable:field name="requestUrl" stringValue="http://flowable.org" />
<flowable:field name="requestHeaders">
<flowable:expression>
<![CDATA[
Accept: text/html
Cache-Control: no-cache
]]>
</flowable:expression>
</flowable:field>
<flowable:field name="requestTimeout">
<flowable:expression>
<![CDATA[
${requestTimeout}
]]>
</flowable:expression>
</flowable:field>
<flowable:field name="resultVariablePrefix">
<flowable:string>task7</flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
错误处理
默认情况下,当发生链接、IO或其他未处理的异常时,Http任务抛出FlowableException。 默认情况下,不会处理任何重定向/客户端/服务端错误状态码。
可以设置failStatusCodes及/或handleStatusCodes字段,配置Http任务处理异常及Http状态的方式。参见配置Http任务。
由handleStatusCodes抛出的BpmnError与其他BPMN异常一样,需要由对应的错误边界事件处理。 下面是一些Http任务错误处理及重试的例子。
400及5XX失败,异步执行,并按照failedJobRetryTimeCycle重试的Http任务
<serviceTask id="failGet" name="Fail test" flowable:async="true" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod">
<flowable:string><![CDATA[GET]]></flowable:string>
</flowable:field>
<flowable:field name="requestUrl">
<flowable:string><![CDATA[http://localhost:9798/api/fail]]></flowable:string>
</flowable:field>
<flowable:field name="failStatusCodes">
<flowable:string><![CDATA[400, 5XX]]></flowable:string>
</flowable:field>
<flowable:failedJobRetryTimeCycle>R3/PT5S</flowable:failedJobRetryTimeCycle>
</extensionElements>
</serviceTask>
将400处理为BmpnError
<serviceTask id="handleGet" name="HTTP Task" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod">
<flowable:string><![CDATA[GET]]></flowable:string>
</flowable:field>
<flowable:field name="requestUrl">
<flowable:string><![CDATA[http://localhost:9798/api/fail]]></flowable:string>
</flowable:field>
<flowable:field name="handleStatusCodes">
<flowable:string><![CDATA[4XX]]></flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
<boundaryEvent id="catch400" attachedToRef="handleGet">
<errorEventDefinition errorRef="HTTP400"></errorEventDefinition>
</boundaryEvent>
忽略异常。
<serviceTask id="ignoreTask" name="Fail test" flowable:type="http">
<extensionElements>
<flowable:field name="requestMethod">
<flowable:string><![CDATA[GET]]></flowable:string>
</flowable:field>
<flowable:field name="requestUrl">
<flowable:string><![CDATA[http://nohost:9798/api]]></flowable:string>
</flowable:field>
<flowable:field name="ignoreException">
<flowable:string><![CDATA[true]]></flowable:string>
</flowable:field>
</extensionElements>
</serviceTask>
异常映射
请参阅异常映射
1.4.8 Mule任务
Mule任务可以向Mule发送消息,增强Flowable的集成特性。请注意Mule任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Mule任务实现为一种特殊的服务任务。
定义骡子任务
Mule任务实现为特殊的服务任务,将服务任务的type定义为*'mule'*进行设置。
<serviceTask id="sendMule" flowable:type="mule">
Mule任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 描述 |
---|---|---|
endpointUrl | 是 | 希望调用的Mule端点(endpoint)。 |
language | 是 | 计算payloadExpression字段所用的语言。 |
payloadExpression | 是 | 消息载荷的表达式。 |
resultVariable | 否 | 存储调用结果的变量名。 |
用法示例
以下XML片段显示了使用Mule Task的示例。
<extensionElements>
<flowable:field name="endpointUrl">
<flowable:string>vm://in</flowable:string>
</flowable:field>
<flowable:field name="language">
<flowable:string>juel</flowable:string>
</flowable:field>
<flowable:field name="payloadExpression">
<flowable:string>"hi"</flowable:string>
</flowable:field>
<flowable:field name="resultVariable">
<flowable:string>theVariable</flowable:string>
</flowable:field>
</extensionElements>
1.4.9 Camel任务
Camel任务(Camel task)可以向Camel发送消息,增强Flowable的集成特性。请注意Camel任务不是BPMN 2.0规范的“官方”任务(所以也没有专用图标)。因此,在Flowable中,Camel任务实现为一种特殊的服务任务。还请注意,需要在项目中包含Flowable Camel模块才能使用Camel任务。
定义Camel任务
Camel任务实现为特殊的服务任务,将服务任务的type定义为*'camel'*进行设置。
<serviceTask id="sendCamel" flowable:type="camel">
只需要在流程定义的服务任务上定义Camel类型即可。集成逻辑都通过Camel容器委托。默认情况下Flowable引擎在Spring容器中查找camelContext Bean。camelContext Bean定义由Camel容器装载的Camel路由。在下面的例子中,按照指定的Java包装载路由。但也可以自行在Spring配置中直接定义路由。
<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<packageScan>
<package>org.flowable.camel.route</package>
</packageScan>
</camelContext>
可以在Camel网站找到关于Camel路由的更多文档。下面只通过几个小例子展示基本概念。在第一个例子中,在Flowable工作流中进行最简单的Camel调用。叫做SimpleCamelCall。
如果想要定义多个Camel上下文Bean,或想使用不同的Bean名字,可以在Camel任务定义中像这样覆盖:
<serviceTask id="serviceTask1" flowable:type="camel">
<extensionElements>
<flowable:field name="camelContext" stringValue="customCamelContext" />
</extensionElements>
</serviceTask>
简单的Camel Call示例
这个例子相关的所有文件,都可以在flowable-camel模块的org.flowable.camel.examples.simpleCamelCall包中找到。目标是简单地启动一个Camel路由。首先需要一个配置了上面提到的路由的Spring上下文。下面的代码实现这个目的:
<camelContext id="camelContext" xmlns="http://camel.apache.org/schema/spring">
<packageScan>
<package>org.flowable.camel.examples.simpleCamelCall</package>
</packageScan>
</camelContext>
public class SimpleCamelCallRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("flowable:SimpleCamelCallProcess:simpleCall").to("log:org.flowable.camel.examples.SimpleCamelCall");
}
}
路由只是记录消息体,不做更多事情。请注意from端点(endpoint)的格式,包含冒号分隔的三个部分:
<process id="SimpleCamelCallProcess">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="simpleCall"/>
<serviceTask id="simpleCall" flowable:type="camel"/>
<sequenceFlow id="flow2" sourceRef="simpleCall" targetRef="end"/>
<endEvent id="end"/>
</process>
连通性测试示例
示例已经可以工作,但实际上Camel与Flowable之间并没有通信,因此没有太多价值。在这个例子里,将试着从Camel接收与发送消息。我们将发送一个字符串,Camel在其上附加一些文字并返回作为结果。发送部分比较普通,即以变量的格式将信息发送给Camel服务。这是我们的调用代码:
@Deployment
public void testPingPong() {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("input", "Hello");
Map<String, String> outputMap = new HashMap<String, String>();
variables.put("outputMap", outputMap);
runtimeService.startProcessInstanceByKey("PingPongProcess", variables);
assertEquals(1, outputMap.size());
assertNotNull(outputMap.get("outputValue"));
assertEquals("Hello World", outputMap.get("outputValue"));
}
“input”变量是实际上是Camel路由的输入,而outputMap用于捕获Camel传回的结果。流程像是这样:
<process id="PingPongProcess">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="ping"/>
<serviceTask id="ping" flowable:type="camel"/>
<sequenceFlow id="flow2" sourceRef="ping" targetRef="saveOutput"/>
<serviceTask id="saveOutput" flowable:class="org.flowable.camel.examples.pingPong.SaveOutput" />
<sequenceFlow id="flow3" sourceRef="saveOutput" targetRef="end"/>
<endEvent id="end"/>
</process>
请注意SaveOutput服务任务会从上下文中取出“Output”变量,并存储至上面提到的OutputMap。现在需要了解变量如何发送至Camel,以及如何返回。这就需要了解Camel行为(Behavior)的概念。变量与Camel通信的方式可以通过CamelBehavior配置。在这个例子里使用默认配置,其它配置在后面会进行简短介绍。可以使用类似的代码配置期望的Camel行为:
<serviceTask id="serviceTask1" flowable:type="camel">
<extensionElements>
<flowable:field name="camelBehaviorClass" stringValue="org.flowable.camel.impl.CamelBehaviorCamelBodyImpl" />
</extensionElements>
</serviceTask>
如果不指定行为,则会设置为org.flowable.camel.impl.CamelBehaviorDefaultImpl。这个行为会将变量复制到相同名字的Camel参数。无论选择什么行为,对于返回值:如果Camel消息体是一个map,则其中的每个元素都将复制为变量;否则整个对象将复制为名为"camelBody"的特定变量。以第二个例子作为Camel路由的总结:
@Override
public void configure() throws Exception {
from("flowable:PingPongProcess:ping").transform().simple("${property.input} World");
}
在这个路由中,字符串"world"会在结尾连接上名为“input”的参数,并将结果作为消息体返回。可以通过Java服务任务访问"camelBody"变量。也可以访问“outputMap”获取。除了这个例子中使用的默认行为之外,我们还可以看看其他的方式。在每个Camel路由开始时,流程实例ID会复制为名为"PROCESS_ID_PROPERTY"的Camel参数。之后会用于将流程实例与Camel路由相关联。也可以在Camel路由中直接使用。
Flowable提供了三种不同的行为。可以通过修改路由URL中特定的部分覆写行为。这里有个在URL中覆写已有行为的例子:
from("flowable:asyncCamelProcess:serviceTaskAsync2?copyVariablesToProperties=true").
下表概述了三种可用的Camel行为:
行为 | URL中 | 描述 |
---|---|---|
CamelBehaviorDefaultImpl | copyVariablesToProperties | 将Flowable变量复制为Camel参数。 |
CamelBehaviorCamelBodyImpl | copyCamelBodyToBody | 只将名为"camelBody"的Flowable变量复制为Camel消息体。 |
CamelBehaviorBodyAsMapImpl | copyVariablesToBodyAsMap | 将一个map中的所有Flowable变量复制为Camel消息体。 |
上表描述了如何将Flowable变量传输到Camel。下表描述了Camel变量如何返回Flowable。这只能在路由URL中配置。
网址 | 描述 |
---|---|
默认 | 如果Camel body是一个map,则将每个元素复制为Flowable变量,否则将整个Camel体复制为“camelBody”Flowable变量 |
copyVariablesFromProperties | 将Camel属性复制为同名的Flowable变量 |
copyCamelBodyToBodyAsString | 至于默认情况,但如果camelBody不是地图,首先将其转换为String然后将其复制到“camelBody” |
copyVariablesFromHeader | 另外,将Camel标头复制到相同名称的Flowable变量 |
返回变量
上面提到的变量传递,不论是从Camel到Flowable还是反向,都只用于变量传递的起始侧。 要特别注意,由于Flowable的行为是非阻塞的,Flowable不会自动向Camel返回变量。 为此提供了特殊的语法。可以在Camel路由URL中,以var.return.someVariableName
的格式,指定一个或多个参数。与这些参数同名(但没有var.return
部分)的变量会作为输出变量。因此将会以相同的名字复制回Camel参数。 例如在如下路由中:
from("direct:start").to("flowable:process?var.return.exampleVar").to("mock:result");
名为exampleVar
的Flowable变量会作为输出变量。因此会以同名复制回Camel参数。
异步连通性测试示例
上面的例子全都是同步的。流程实例等待,直到Camel路由结束并返回。有时,需要Flowable流程实例继续运行。这时可以使用Camel服务任务的异步功能。可以将Camel服务任务的async参数设置为true,启用这个功能。
<serviceTask id="serviceAsyncPing" flowable:type="camel" flowable:async="true"/>
设置这个参数后,Camel路由会由Flowable作业执行器异步启动。如果在Camel路由中定义了队列,Flowable流程实例会继续执行流程定义中Camel服务任务之后的活动。Camel路由会与流程执行完全异步地执行。如果需要在流程定义的某处等待Camel服务任务的响应,可以使用接收任务(receive task)。
<receiveTask id="receiveAsyncPing" name="Wait State" />
流程实例会等待,直到接收到来自Camel的信号。可以在Camel中向特定的Flowable端点发送消息,来为流程实例发送信号。
from("flowable:asyncPingProcess:serviceAsyncPing").to("flowable:asyncPingProcess:receiveAsyncPing");
- “flowable”字符串常量
- 流程名
- 接收任务名
使用Camel路由实例化工作流
上面的例子都是先启动Flowable流程实例,然后在流程实例中启动Camel路由。也可以反过来,在已经启动的Camel路由中启动或调用流程实例。类似于为接收任务发送消息。例如,一个简单的路由:
from("direct:start").to("flowable:camelProcess");
可以看到,URL包含两部分:第一部分是“flowable”字符串常量,第二部分是流程定义的名字。当然,需要提前在Flowable引擎中部署这个流程定义。
也可以在Camel头中,将流程实例起动人设置为某个已认证用户ID。为此,首先需要在流程定义中指定启动人变量:
<startEvent id="start" flowable:initiator="initiator" />
然后使用Camel头CamelProcessInitiatorHeader指定用户ID。Camel路由定义如下:
from("direct:startWithInitiatorHeader")
.setHeader("CamelProcessInitiatorHeader", constant("kermit"))
.to("flowable:InitiatorCamelCallProcess?processInitiatorHeaderName=CamelProcessInitiatorHeader");
1.4.10 手动任务
描述
*手动任务(manual task)*定义在BPM引擎之外的任务。它用于建模引擎不需要了解,也不需要提供系统或用户界面的工作。对于引擎来说,手动任务将按直接穿过活动处理,在流程执行到达手动任务时,自动继续执行流程。
图形符号
手动任务可视化为圆角矩形,左上角有一个小手形图标
XML表示
<manualTask id="myManualTask" name="Call client for more information" />
1.4.11 Java接收任务
描述
接收任务(receive task),是等待特定消息到达的简单任务。目前,我们只为这个任务实现了Java语义。当流程执行到达接收任务时,流程状态将提交至持久化存储。这意味着流程将保持等待状态,直到引擎接收到特定的消息,触发流程穿过接收任务继续执行。
图形符号
接收任务用左上角有一个消息图标的标准BPMN 2.0任务(圆角矩形)表示。消息图标是白色的(对应的黑色消息图标代表发送的含义)。
XML表示
<receiveTask id="waitState" name="wait" />
要使流程实例从接收任务的等待状态中继续执行,需要使用到达接收任务的执行id,调用runtimeService.signal(executionId)。下面的代码片段展示了如何操作:
ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
Execution execution = runtimeService.createExecutionQuery()
.processInstanceId(pi.getId())
.activityId("waitState")
.singleResult();
assertNotNull(execution);
runtimeService.trigger(execution.getId());
1.4.12 Shell任务
描述
Shell任务(Shell task)可以运行Shell脚本与命令。请注意Shell任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。
定义Shell任务
Shell任务实现为特殊的服务任务,将服务任务的type定义为*'shell'*进行设置。
<serviceTask id="shellEcho" flowable:type="shell">
Shell任务通过字段注入配置。这些参数的值可以使用EL表达式,将在流程执行运行时解析。可以设置下列参数:
参数 | 必填? | 类型 | 描述 | 默认值 |
---|---|---|---|---|
command | 是 | String | 要执行的Shell命令。 | |
arg0-5 | 否 | String | 参数0至参数5 | |
wait | 否 | true/false | 是否等待Shell进程终止。 | true |
redirectError | 否 | true/false | 是否将标准错误(standard error)并入标准输出(standard output)。 | false |
cleanEnv | 否 | true/false | 是否避免Shell进程继承当前环境。 | false |
outputVariable | 否 | String | 保存输出的变量名 | 不会记录输出。 |
errorCodeVariable | 否 | String | 保存结果错误码的变量名 | 不会记录错误码。 |
directory | 否 | String | Shell进程的默认目录 | 当前目录 |
示例
下面的XML代码片段是使用Shell任务的例子。将会运行"cmd /c echo EchoTest" Shell脚本,等待其结束,并将其结果存入resultVar。
<serviceTask id="shellEcho" flowable:type="shell" >
<extensionElements>
<flowable:field name="command" stringValue="cmd" />
<flowable:field name="arg1" stringValue="/c" />
<flowable:field name="arg2" stringValue="echo" />
<flowable:field name="arg3" stringValue="EchoTest" />
<flowable:field name="wait" stringValue="true" />
<flowable:field name="outputVariable" stringValue="resultVar" />
</extensionElements>
</serviceTask>
1.4.13 执行监听器
执行监听器(execution listener)可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式。可以被捕获的事件有:
- 流程实例的启动和结束。
- 流程执行转移。
- 活动的启动和结束。
- 网关的启动和结束。
- 中间事件的启动和结束。
- 启动事件的结束,和结束事件的启动。
下面的流程定义包含了三个执行监听器:
<process id="executionListenersProcess">
<extensionElements>
<flowable:executionListener
class="org.flowable.examples.bpmn.executionlistener.ExampleExecutionListenerOne"
event="start" />
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="secondTask">
<extensionElements>
<flowable:executionListener
class="org.flowable.examples.bpmn.executionListener.ExampleExecutionListenerTwo" />
</extensionElements>
</sequenceFlow>
<userTask id="secondTask" >
<extensionElements>
<flowable:executionListener
expression="${myPojo.myMethod(execution.event)}"
event="end" />
</extensionElements>
</userTask>
<sequenceFlow sourceRef="secondTask" targetRef="thirdTask" />
<userTask id="thirdTask" />
<sequenceFlow sourceRef="thirdTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
第一个执行监听器将在流程启动时收到通知。这个监听器是一个外部Java类(ExampleExecutionListenerOne
),并且需要实现org.flowable.engine.delegate.ExecutionListener
接口。当该事件发生时(这里是start
事件),会调用notify(ExecutionListenerExecution execution)
方法。
public class ExampleExecutionListenerOne implements ExecutionListener {
public void notify(ExecutionListenerExecution execution) throws Exception {
execution.setVariable("variableSetInExecutionListener", "firstValue");
execution.setVariable("eventReceived", execution.getEventName());
}
}
也可以使用实现了org.flowable.engine.delegate.JavaDelegate
接口的委托类。这些委托类也可以用于其他的结构,如服务任务的委托。
第二个执行监听器在流程执行转移时被调用。请注意listener
元素并未定义event
,因为在转移上只会触发take
事件。当监听器定义在转移上时,event属性的值将被忽略。
最后一个执行监听器在secondTask
活动结束时被调用。监听器声明中没有使用class
,而是定义了expression
。这个表达式将在事件触发时计算/调用。
<flowable:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />
与其他表达式一样,可以使用与解析execution变量。execution对象提供了露事件名参数,可以使用execution.eventName
向你的方法传递事件名。
与服务任务类似,执行监听器也支持使用delegateExpression
。
<flowable:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />
较早之前,我们也引入了新的执行监听器类型,org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener。这个脚本执行监听器可以为执行监听器事件执行一段脚本代码。
<flowable:executionListener event="start"
class="org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener">
<flowable:field name="script">
<flowable:string>
def bar = "BAR"; // local variable
foo = "FOO"; // pushes variable to execution context
execution.setVariable("var1", "test"); // test access to execution instance
bar // implicit return value
</flowable:string>
</flowable:field>
<flowable:field name="language" stringValue="groovy" />
<flowable:field name="resultVariable" stringValue="myVar" />
</flowable:executionListener>
执行监听器上的字段注入
使用通过class
属性配置的执行监听器时,可以使用字段注入。与服务任务字段注入使用完全相同的机制,可以在那里看到字段注入的各种用法。
下面的代码片段展示了一个简单的示例流程,带有一个使用了字段注入的执行监听器。
<process id="executionListenersProcess">
<extensionElements>
<flowable:executionListener
class="org.flowable.examples.bpmn.executionListener.ExampleFieldInjectedExecutionListener"
event="start">
<flowable:field name="fixedValue" stringValue="Yes, I am " />
<flowable:field name="dynamicValue" expression="${myVar}" />
</flowable:executionListener>
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
public class ExampleFieldInjectedExecutionListener implements ExecutionListener {
private Expression fixedValue;
private Expression dynamicValue;
public void notify(ExecutionListenerExecution execution) throws Exception {
execution.setVariable("var", fixedValue.getValue(execution).toString() +
dynamicValue.getValue(execution).toString());
}
}
ExampleFieldInjectedExecutionListener
类将连接两个字段(一个是固定值-fixedValue,另一个是动态值-dynamicValue),并将其存储在'var
'流程变量中。
@Deployment(resources = {
"org/flowable/examples/bpmn/executionListener/ExecutionListenersFieldInjectionProcess.bpmn20.xml"})
public void testExecutionListenerFieldInjection() {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("myVar", "listening!");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
"executionListenersProcess", variables);
Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
assertNotNull(varSetByListener);
assertTrue(varSetByListener instanceof String);
// Result is a concatenation of fixed injected field and injected expression
assertEquals("Yes, I am listening!", varSetByListener);
}
请注意,与线程安全相关的相同规则适用于服务任务。有关更多信息,请阅读相关部分。
1.4.14 任务监听器
*任务监听器(task listener)*用于在特定的任务相关事件发生时,执行自定义的Java逻辑或表达式。
任务监听器只能在流程定义中作为用户任务的子元素。请注意,任务监听器是一个Flowable自定义结构,因此也需要作为BPMN 2.0 extensionElements,放在flowable命名空间下。
<userTask id="myTask" name="My Task" >
<extensionElements>
<flowable:taskListener event="create" class="org.flowable.MyTaskCreateListener" />
</extensionElements>
</userTask>
任务监听器包含下列属性:
- event(事件)(必填):触发任务监听器的任务事件类型。可用的事件有:
- create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。
- assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,在触发create事件之前,会首先触发assignment事件。这顺序看起来不太自然,但是有实际原因的:当收到create事件时,我们通常希望能看到任务的所有参数,包括办理人。
- complete(完成):当任务已经完成,从运行时数据中删除前触发。
- delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。
- class:需要调用的委托类。这个类必须实现
org.flowable.engine.delegate.TaskListener
接口。
public class MyTaskCreateListener implements TaskListener {
public void notify(DelegateTask delegateTask) {
// Custom logic goes here
}
}
也可以使用字段注入,为委托类传递流程变量或执行。请注意委托类的实例在流程部署时创建(与Flowable中其它的委托类一样),这意味着该实例会在所有流程实例执行中共享。
- expression:(不能与class属性一起使用):指定在事件发生时要执行的表达式。可以为被调用的对象传递
DelegateTask
对象与事件名(使用task.eventName
)作为参数。
<flowable:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
- delegateExpression:指定一个能够解析为
TaskListener
接口实现类的对象的表达式。与服务任务类似。
<flowable:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
- 较早之前,我们也引入了新的执行监听器类型,org.flowable.engine.impl.bpmn.listener.ScriptTaskListener。这个脚本任务监听器可以为一个任务监听器事件执行一段脚本代码。
<flowable:taskListener event="complete" class="org.flowable.engine.impl.bpmn.listener.ScriptTaskListener" >
<flowable:field name="script">
<flowable:string>
def bar = "BAR"; // local variable
foo = "FOO"; // pushes variable to execution context
task.setOwner("kermit"); // test access to task instance
bar // implicit return value
</flowable:string>
</flowable:field>
<flowable:field name="language" stringValue="groovy" />
<flowable:field name="resultVariable" stringValue="myVar" />
</flowable:taskListener>
1.4.15 多实例(for each)
描述
实例活动(multi-instance activity)是在业务流程中,为特定步骤定义重复的方式。在编程概念中,多实例类似for each结构:可以为给定集合中的每一条目,顺序或并行地,执行特定步骤,甚至是整个子流程。
多实例是一个普通活动,加上定义(被称作“多实例特性的”)额外参数,会使得活动在运行时被多次执行。下列活动可以成为多实例活动:
按照BPMN2.0规范的要求,用于为每个实例创建执行的父执行,会提供下列变量:
- nrOfInstances:实例总数。
- nrOfActiveInstances:当前活动的(即未完成的),实例数量。对于顺序多实例,这个值总为1。
- nrOfCompletedInstances:已完成的实例数量。
可以调用execution.getVariable(x)
方法获取这些值。
另外,每个被创建的执行,都有局部变量(对其他执行不可见,也不存储在流程实例级别):
- loopCounter:给定实例在for-each循环中的index。可以通过Flowable的elementIndexVariable属性为loopCounter变量重命名。
图示
如果一个活动是多实例,将通过在该活动底部的三条短线表示。三条竖线代表实例会并行执行,而三条横线代表顺序执行。
XML表示
要创建活动多实例,活动XML元素必须具有multiInstanceLoopCharacteristics
子元素。
<multiInstanceLoopCharacteristics isSequential="false|true">
...
</multiInstanceLoopCharacteristics>
isSequential属性代表了活动的实例为顺序还是并行执行。
实例的数量在进入活动时,计算一次。有几种不同方法可以配置数量。一个方法是通过loopCardinality子元素,直接指定数字。
<multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>5</loopCardinality>
</multiInstanceLoopCharacteristics>
也可以使用解析为正整数的表达式:
<multiInstanceLoopCharacteristics isSequential="false|true">
<loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>
另一个定义实例数量的方法,是使用loopDataInputRef
子元素,指定一个集合型流程变量的名字。对集合中的每一项,都会创建一个实例。可以使用inputDataItem
子元素,将该项设置给该实例的局部变量。在下面的XML示例中展示:
<userTask id="miTasks" name="My Task ${loopCounter}" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>assigneeList</loopDataInputRef>
<inputDataItem name="assignee" />
</multiInstanceLoopCharacteristics>
</userTask>
假设变量assigneeList
包含[kermit, gonzo, fozzie]
。上面的代码会创建三个并行的用户任务。每一个执行都有一个名为assignee
的(局部)流程变量,含有集合中的一项,并在这个例子中被用于指派用户任务。
loopDataInputRef
与inputDataItem
的缺点是名字很难记,并且由于BPMN 2.0概要的限制,不能使用表达式。Flowable通过在multiInstanceCharacteristics
上提供collection与elementVariable属性解决了这些问题:
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="${myService.resolveUsersForTask()}" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
请注意collection
属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,这个字符串都会被当做变量名,在流程变量中用于获取实际的集合。
例如,下面的代码片段会要求集合存储在assigneeList
流程变量中:
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
假如myService.getCollectionVariableName()
返回字符串值,引擎就会用这个值作为变量名,获取流程变量保存的集合。
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="${myService.getCollectionVariableName()}" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>
多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例活动,继续执行流程。这个表达式必须通过completionCondition子元素定义。
<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
在这个例子里,会为assigneeList
集合中的每个元素创建并行实例。当60%的任务完成时,其他的任务将被删除,流程继续运行。
边界事件和多实例
多实例是普通活动,因此可以在其边界定义边界事件。如果是中断边界事件,当其捕获事件时,会销毁活动中的所有实例。以下面的多实例子流程为例:
这里,当计时器触发时,子进程的所有实例都将被销毁,无论有多少实例或哪些内部活动尚未完成。
多实例与执行监听器
执行监听器与多实例一起使用时需要特别注意。以下面的BPMN 2.0 XML代码片段为例。这段XML定义在与multiInstanceLoopCharacteristics XML元素相同的级别:
<extensionElements>
<flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
<flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
</extensionElements>
对于普通的BPMN活动,会在活动开始与结束时调用一次监听器。
但是当该活动为多实例时,行为有区别:
- 当进入多实例活动时,在任何内部活动执行前,抛出一个启动事件。loopCounter变量还未设置(为null)。
- 进入每个实际执行的活动时,抛出一个启动事件。loopCounter变量已经设置。
结束事件类似:
- 离开每个实际执行的活动后,抛出一个结束事件。loopCounter变量已经设置。
- 多实例活动整体完成后,抛出一个结束事件。loopCounter变量未设置。
例如:
<subProcess id="subprocess1" name="Sub Process">
<extensionElements>
<flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
<flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
</extensionElements>
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>assignees</loopDataInputRef>
<inputDataItem name="assignee"></inputDataItem>
</multiInstanceLoopCharacteristics>
<startEvent id="startevent2" name="Start"></startEvent>
<endEvent id="endevent2" name="End"></endEvent>
<sequenceFlow id="flow3" name="" sourceRef="startevent2" targetRef="endevent2"></sequenceFlow>
</subProcess>
在这个例子中,假设assignees有三项。在运行时会发生如下事情:
- 多实例整体抛出一个启动事件。调用一次start执行监听器,loopCounter与assignee变量均未设置(即为null)。
- 每一个活动实例抛出一个启动事件。调用三次start执行监听器,loopCounter与assignee变量均已设置(也就是说不为null)。
- 因此启动执行监听器总共被调用四次。
请注意,即使multiInstanceLoopCharacteristics不是定义在子流程上,也是一样。例如,如果上面的例子中只是一个简单的用户任务,抛出事件的行为也是一样。
1.4.16 补偿处理器
描述
如果要使用一个活动补偿另一个活动的影响,可以将其声明为补偿处理器(compensation handler)。补偿处理器不在正常流程中执行,而只在流程抛出补偿事件时才会执行。
补偿处理器不得有入口或出口顺序流。
补偿处理器必须通过单向的连接,关联一个补偿边界事件。
图示
如果一个活动是补偿处理器,则会在其下部中间显示补偿事件图标。下面摘录的流程图展示了一个带有补偿边界事件的服务任务,并关联至一个补偿处理器。请注意补偿处理器图标显示在"cancel hotel reservation(取消酒店预订)"服务任务的下部中间。
XML表示
要将一个活动声明为补偿处理器,需要将isForCompensation
属性设置为true:
<serviceTask id="undoBookHotel" isForCompensation="true" flowable:class="...">
</serviceTask>
1.5 子流程与调用活动
1.5.1 子流程
描述
子流程(sub-process)是包含其他的活动、网关、事件等的活动。其本身构成一个流程,并作为更大流程的一部分。子流程完全在父流程中定义(这就是为什么经常被称作嵌入式子流程)。
子流程有两个主要的使用场景:
- 子流程可以分层建模。很多建模工具都可以折叠子流程,隐藏子流程的所有细节,而只显示业务流程的高层端到端总览。
- 子流程会创建新的事件范围。在子流程执行中抛出的事件可以通过子流程边界上的边界事件捕获,为该事件创建了限制在子流程内的范围。
使用子流程也要注意以下几点:
- 子流程只能有一个空启动事件,而不允许有其他类型的启动事件。请注意BPMN 2.0规范允许省略子流程的启动与结束事件,但目前Flowable的实现尚不支持省略。
- 顺序流不能跨越子流程边界。
图示
子流程表示为标准活动(圆角矩形)。若折叠了子流程,则只显示其名字与一个加号,以展示流程的高层概览:
若展开了子流程,则在子流程内显示子流程的所有步骤:
使用子流程的一个主要原因是为事件定义范围。下面的流程模型展示了这种用法:*investigate software(调查硬件)/investigate hardware(调查软件)两个任务需要并行执行,且需要在给定时限内,在Level 2 support(二级支持)*响应前完成。在这里,定时器的范围(即需要按时完成的活动)通过子流程进行限制。
XML表示
子流程通过subprocess元素定义。子流程中的所有活动、网关、事件等,都需要定义在这个元素内。
<subProcess id="subProcess">
<startEvent id="subProcessStart" />
... other Sub-Process elements ...
<endEvent id="subProcessEnd" />
</subProcess>
1.5.2 事件子流程
描述
事件子流程(event sub-process)是BPMN 2.0新定义的。事件子流程是通过事件触发的子流程。可以在流程级别,或者任何子流程级别,添加事件子流程。用于触发事件子流程的事件,使用启动事件进行配置。因此可知,不能在事件子流程中使用空启动事件。事件子流程可以通过消息事件、错误事件、信号时间、定时器事件或补偿事件等触发。在事件子流程的宿主范围(流程实例或子流程)创建时,创建对启动事件的订阅。当该范围销毁时,删除订阅。
事件子流程可以是中断或不中断的。中断的子流程将取消当前范围内的任何执行。非中断的事件子流程将创建新的并行执行。宿主范围内的每个活动,只能触发一个中断事件子流程,而非中断事件子流程可以多次触发。子流程是否是中断的,通过触发事件子流程的启动事件配置。
事件子流程不能有任何入口或出口顺序流。事件子流程是由事件触发的,因此入口顺序流不合逻辑。当事件子流程结束时,要么同时结束当前范围(中断事件子流程的情况),要么是非中断子流程创建的并行执行结束。
目前的限制:
- Flowable支持错误、定时器、信号与消息启动事件触发事件子流程。
图示
事件子流程表示为点线边框的嵌入式子流程。
XML表示
事件子流程的XML表示形式与嵌入式子流程相同。但需要将triggeredByEvent
属性设置为true
:
<subProcess id="eventSubProcess" triggeredByEvent="true">
...
</subProcess>
例
下面是使用错误启动事件触发事件子流程的例子。该事件子流程处于“流程级别”,即流程实例的范围:
事件子流程在XML中是这样的:
<subProcess id="eventSubProcess" triggeredByEvent="true">
<startEvent id="catchError">
<errorEventDefinition errorRef="error" />
</startEvent>
<sequenceFlow id="flow2" sourceRef="catchError" targetRef="taskAfterErrorCatch" />
<userTask id="taskAfterErrorCatch" name="Provide additional data" />
</subProcess>
前面已经指出,事件子流程也可以添加到嵌入式子流程内。若添加到嵌入式子流程内,可以代替边界事件的功能。例如在下面两个流程图中,嵌入式子流程都抛出错误事件。错误事件都被捕获,并由用户任务处理。
相反:
两种情况下都执行相同的任务。然而,两种模型有如下不同:
- 嵌入式(事件)子流程使用其宿主范围的执行来执行。这意味着嵌入式(事件)子流程可以访问其范围的局部变量。当使用边界事件时,执行嵌入式子流程的执行,会被边界事件的出口顺序流删除。意味着嵌入式子流程创建的变量将不再可用。
- 使用事件子流程时,事件完全由其所在的子流程处理。当使用边界事件时,事件由其父流程处理。
这两点可以帮助你判断哪种方式更适合解决特定的流程建模或实现问题,以选择使用边界事件还是嵌入式(事件)子流程。
1.5.3 事务子流程
描述
事务子流程(transaction sub-process)是一种嵌入式子流程,用于将多个活动组织在一个事务里。事务是工作的逻辑单元,可以组织一组独立活动,使得它们可以一起成功或失败。
**事务的可能结果:**事务有三种不同的结果:
- 若未被取消,或被意外终止,则事务成功(successful)。若事务子流程成功,将使用出口顺序流离开。若流程后面抛出了补偿事件,成功的事务可以被补偿。*请注意:*与“普通”嵌入式子流程一样,可以使用补偿抛出中间事件,在事务成功完成后补偿。
- 若执行到达取消结束事件时,事务被取消(canceled)。在这种情况下,所有执行都将被终止并移除。只会保留一个执行,设置为取消边界事件,并将触发补偿。在补偿完成后,事务子流程通过取消边界事件的出口顺序流离开。
- 若由于抛出了错误结束事件,且未被事务子流程所在的范围捕获,则事务会被*意外(hazard)*终止。错误被事件子流程的边界捕获也一样。在这种情况下,不会进行补偿。
下面的流程图展示这三种不同的结果:
**与ACID事务的关系:**要注意不要将BPMN事务子流程与技术(ACID)事务混淆。BPMN事务子流程不是划分技术事务范围的方法。要理解Acitivit中的事务管理,请阅读并发与事务章节。BPMN事务与ACID事务有如下区别:
- ACID事务生存期一般比较短,而BPMN事务可以持续几小时,几天甚至几个月才完成。考虑一个场景:事务包括的活动中有一个用户任务。通常人的响应时间要比程序长。或者,在另一个场景下,BPMN事务可能等待某些业务事件发生,像是特定订单的填写完成。这些操作通常要比更新数据库字段、使用事务队列存储消息等,花长得多的时间完成。
- 不可能将业务活动的持续时间限定为ACID事务的范围,因此一个BPMN事务通常会生成多个ACID事务。
- 一个BPMN事务可以生成多个ACID事务,也就不能使用ACID特性。例如,考虑上面的流程例子。假设"book hotel(预订酒店)"与"charge credit card(信用卡付款)"操作在分开的ACID事务中处理。再假设"book hotel(预订酒店)"活动已经成功。这时,因为已经进行了预订酒店操作,而还没有进行信用卡扣款,就处在中间不一致状态(intermediary inconsistent state)。在ACID事务中,会顺序进行不同的操作,因此也处在中间不一致状态。在这里不一样的是,不一致状态在事务范围外可见。例如,如果通过外部预订服务进行预定,则使用该预订服务的其他部分将能看到酒店已被预订。这意味着,当使用业务事务时,完全不会使用隔离参数(的确,当使用ACID事务时,我们通常也会降低隔离级别,以保证高并发级别。但ACID事务可以细粒度地进行控制,而中间不一致状态也只会存在于一小段时间内)。
- BPMN业务事务不使用传统方式回滚。这是因为它生成多个ACID事务,在BPMN事务取消时,部分ACID事务可能已经提交。这样它们没法回滚。
因为BPMN事务天生需要长时间运行,因此就需要使用不同的方式缺乏隔离与回滚机制造成的问题。在实际使用中,通常只能通过领域特定(domain specific)的方式解决这些问题:
- 通过补偿实现回滚。如果在事务范围内抛出了取消事件,就补偿所有成功执行并带有补偿处理器的活动所造成的影响。
- 缺乏隔离通常使用特定领域的解决方案来处理。例如,在上面的例子里,在我们确定第一个客户可以付款前,一个酒店房间可能被第二个客户预定。这可能不满足业务预期,因此预订服务可能会选择允许一定量的超量预定。
- 另外,由于事务可以由于意外而终止,预订服务需要处理这种情况,比如酒店房间已经预定,但从未付款(因为事务可能已经终止)。在这种情况下,预定服务可能选择这种策略:一个酒店房间有最大预留时间,若到时还未付款,则取消预订。
总结一下:尽管ACID事务提供了对回滚、隔离级别,与启发式结果(heuristic outcomes)等问题的通用解决方案,但仍然需要在实现业务事务时,为这些问题寻找特定领域的解决方案。
目前的限制:
- BPMN规范要求,流程引擎响应底层事务协议提交的事务。如果在底层协议中发生了取消事件,则取消事务。作为嵌入式的引擎,Flowable当前不支持这点。查看下面介绍一致性的段落,了解其后果。
**基于ACID事务与乐观锁(optimistic concurrency)的一致性:**BPMN事务在如下情况保证一致性:所有活动都成功完成;或若部分活动不能执行,则所有已完成活动都被补偿。两种方法都可以达到最终一致性状态。然而需要了解的是:Flowable中BPMN事务的一致性模型,以流程执行的一致性模型为基础。Flowable以事务的方式执行流程,并通过乐观锁标记处理并发。在Flowable中,BPMN的错误、取消与补偿事件,都建立在相同的ACID事务与乐观锁之上。例如,只有在实际到达时,取消结束事件才能触发补偿。如果服务任务抛出了非受检异常,导致并未实际到达取消结束事件;或者,由于底层ACID事务中的其他操作,将事务设置为rollback-only(回滚)状态,导致补偿处理器的操作不能提交;或者,当两个并行执行到达一个取消结束事件时,补偿会被两次触发,并由于乐观锁异常而失败。这些情况下都不能真正完成补偿。想说明的是,当在Flowable中实现BPMN事务时,与实施“普通”流程与子流程,需要遵守相同的规则。因此实现流程时需要有效地保证一致性,需要将乐观锁、事务执行模型纳入考虑范围。
图示
事务子流程表示为带有两层边框的嵌入式子流程。
XML表示
事务子流程在XML中通过transaction
标签表示:
<transaction id="myTransaction" >
...
</transaction>
例
以下是事务子流程的示例:
1.5.4 调用活动(子流程)
描述
尽管看起来很相像,但在BPMN 2.0中,调用活动(call activity)有别于一般的子流程——通常也称作嵌入式子流程。从概念上说,两者都在流程执行到达该活动时,调用一个子流程。
两者的区别为,调用活动引用一个流程定义外部的流程,而子流程嵌入在原有流程定义内。调用活动的主要使用场景是,在多个不同流程定义中调用一个可复用的流程定义。
当流程执行到达调用活动时,会创建一个新的执行,作为到达调用活动的执行的子执行。这个子执行用于执行子流程,也可用于创建并行子执行(与普通流程中行为类似)。父执行将等待子流程完成,之后沿原流程继续执行。
图示
调用活动表现为带有粗边框(折叠与展开都是)的子流程。取决于建模工具,调用活动可以展开,但默认表现为折叠形式。
XML表示
调用活动是一个普通活动,在calledElement中通过key引用流程定义。在实际使用中,通常在calledElement中配置流程的ID。
<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
请注意子流程的流程定义在运行时解析。这意味着如果需要的话,子流程可以与调用流程分别部署。
传递变量
可以向子流程传递与接收流程变量。数据将在子流程启动时复制到子流程,并在其结束时复制回主流程。
<callActivity id="callSubProcess" calledElement="checkCreditProcess">
<extensionElements>
<flowable:in source="someVariableInMainProcess"
target="nameOfVariableInSubProcess" />
<flowable:out source="someVariableInSubProcess"
target="nameOfVariableInMainProcess" />
</extensionElements>
</callActivity>
您可以通过将选项设置inheritVariables
为true 将所有流程变量传递给子流程。
<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:inheritVariables="true"/>
除了需要按照BPMN 2.0标准的方式声明流程变量的BPMN标准元素dataInputAssociation与dataOutputAssociation之外, Flowable还提供了扩展作为快捷方式。
也可以在这里使用表达式:
<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
<extensionElements>
<flowable:in sourceExpression="${x+5}" target="y" />
<flowable:out source="${y+5}" target="z" />
</extensionElements>
</callActivity>
因此最终 z = y+5 = x+5+5 。
调用活动元素还提供了一个自定义Flowable属性扩展,businessKey,用于设置子流程实例的businessKey。
<callActivity id =“callSubProcess”calledElement =“checkCreditProcess”flowable:businessKey =“$ {myVariable}”>
...
</ callActivity>
将inheritBusinessKey属性设置为true
,会将子流程的businessKey值设置为调用流程的businessKey的值。
<callActivity id =“callSubProcess”calledElement =“checkCreditProcess”flowable:inheritBusinessKey =“true”>
...
</ callActivity>
引用同一部署中的流程
默认会使用引用流程最后部署的流程定义版本。但有的时候也会想引用与主流程一起部署的引用流程定义。这需要将主流程与引用流程放在同一个部署单元中,以便引用相同的部署。
在callActivity
元素中,将sameDeployment
属性设置为true
,即可引用相同部署的流程。
如下例所示:
<callActivity id =“callSubProcess”calledElement =“checkCreditProcess”flowable:sameDeployment =“true”>
...
</ callActivity>
sameDeployment
默认值为false。
示例
下面的流程图展示了简单的订单处理流程。因为检查客户的信用额度的操作在许多其他流程中都通用,因此将*check credit step(检查信用额度步骤)*建模为调用活动。
<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:fallbackToDefaultTenant="true" >
...
</callActivity>
默认值为false
。
通过id引用流程定义
默认行为是采用最新部署的流程定义来按键启动引用的流程。在某些用例中,您希望通过其id引用精确的流程定义。
要通过id使用引用的流程定义,请使用flowable:calledElementType
值id
。允许的值是key
和id
。
<callActivity id =“callSubProcess”calledElement =“UNIQUE-PROCESS_DEFINITION-ID”flowable:calledElementType =“id”>
...
</ callActivity>
默认情况下,该flowable:calledElementType
属性设置为key
。
示例
下面的流程图展示了简单的订单处理流程。因为检查客户的信用额度的操作在许多其他流程中都通用,因此将*check credit step(检查信用额度步骤)*建模为调用活动。
流程像是下面这样:
<startEvent id="theStart" />
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="receiveOrder" />
<manualTask id="receiveOrder" name="Receive Order" />
<sequenceFlow id="flow2" sourceRef="receiveOrder" targetRef="callCheckCreditProcess" />
<callActivity id="callCheckCreditProcess" name="Check credit" calledElement="checkCreditProcess" />
<sequenceFlow id="flow3" sourceRef="callCheckCreditProcess" targetRef="prepareAndShipTask" />
<userTask id="prepareAndShipTask" name="Prepare and Ship" />
<sequenceFlow id="flow4" sourceRef="prepareAndShipTask" targetRef="end" />
<endEvent id="end" />
子流程如下:
子流程的流程定义没有什么特别之处。它也可以在不被另一个进程调用的情况下使用。
异步完成
在某些特定用例中,需要使调用活动的完成异步(和独占)。
采用以下流程定义。假设被调用的流程定义仅包含标记为异步和独占的服务任务。这些子流程实例的所有步骤都将异步执行。执行最后一个异步服务任务时,将访问结束事件。此时,控件将传递回父流程实例。
问题是异步锁定发生在流程实例级别上。在这种情况下,访问最后一个服务任务和事件时将锁定子流程实例。这意味着当控件传递回父流程实例时,因此当调用活动完成时,父流程实例不会被锁定。使用多实例时,这意味着x个实例尝试完成调用活动。即使在所有步骤中使用独占,这仍然意味着每个子流程实例将同时尝试更新父进程实例,可能导致FlowableOptimisticLockingExceptions,因为错误的流程实例用于独占锁。
此问题的解决方案是标记调用活动的异步完成标志。如果为true,则将使用父流程实例创建新的异步作业。创建独占锁时,将为父进程实例保留它,并且将分别为每个子执行单独处理调用活动的多实例完成。
1.6 事务和并发
1.6.1 异步延续
(Asynchronous Continuations)
Flowable以事务方式执行流程,并可按照你的需求配置。让我们从Flowable一般如何为事务划分范围开始介绍。如果Flowable被触发(启动流程,完成任务,为执行发送信号),Flowable将沿流程执行,直到到达每个执行路径的等待状态。更具体地说,它以深度优先方式搜索流程图,并在每个执行分支都到达等待状态时返回。等待状态是“之后”再执行的任务,也就是说着Flowable将当前执行持久化,并等待再次触发。触发可以来自外部来源如用户任务或消息接受任务,也可以来自Flowable自身如定时器事件。以下面的图片说明:
这是一个BPMN流程的片段,有一个用户任务、一个服务任务,与一个定时器事件。用户任务的完成操作与验证地址(validate address)在同一个工作单元内,因此需要原子性地(atomically)成功或失败。这意味着如果服务任务抛出了异常,我们会想要回滚当前事务,以便执行返回到用户任务,并希望用户任务仍然保存在数据库中。这也是Flowable的默认行为。在(1)中,应用或客户端线程完成任务。在相同的线程中,Flowable执行服务并继续,直到到达等待状态,在这个例子中,是定时器事件(2)。然后将控制权返回至调用者(3),同时提交事务(如果事务由Flowable开启)。
在有的情况下,我们不想要这样。有时我们需要在流程中自定义地控制事务边界,以便为工作的逻辑单元划分范围。这就需要使用异步延续。考虑下面的流程(片段):
完成用户任务,生成发票,并将发票发送给客户。这次发票的生成不再是同一个工作单元的一部分,因此我们不希望当发票生成失败时,回滚用户任务。所以我们希望Flowable做的,是完成用户任务(1),提交事务,并将控制权返回给调用程序。然后我们希望在后台线程中,异步地生成发票。这个后台线程就是Flowable作业执行器(事实上是一个线程池),它周期性地将作业保存至数据库。因此在幕后,当到达"generate invoice(生成发票)"任务时,Flowable会创建“消息”作业并将其持久化到数据库中,用于继续执行流程。这个作业之后会被作业执行器选中并执行。Flowable也会向本地的作业执行器进行提示,告知其有新作业到来,以提升性能。
要使用这个特性,可以使用*flowable:async="true"*扩展。因此,一个示例的服务任务会像是这样:
<serviceTask id="service1" name="Generate Invoice"
flowable:class="my.custom.Delegate"
flowable:async="true" />
可以为下列BPMN任务类型指定flowable:async:任务,服务任务,脚本任务,业务规则任务,发送任务,接收任务,用户任务,子流程,调用活动。
对于用户任务、接收任务与其他等待状态来说,异步操作允许我们在一个独立的线程/事务中启动执行监听器。
1.6.2 失败重试
默认配置下,如果作业执行中有任何异常,Flowable将三次重试执行作业。对异步作业也是这样。需要更灵活的配置时可以使用这两个参数:
- 重试的次数
- 重试的间隔
这两个参数可以通过flowable:failedJobRetryTimeCycle
元素配置。这有一个简单的例子:
<serviceTask id="failingServiceTask" flowable:async="true"
flowable:class="org.flowable.engine.test.jobexecutor.RetryFailingDelegate">
<extensionElements>
<flowable:failedJobRetryTimeCycle>R5/PT7M</flowable:failedJobRetryTimeCycle>
</extensionElements>
</serviceTask>
时间周期表达式与定时器事件表达式一样遵循ISO 8601标准。上面的例子会让作业执行器重试5次,并在每次重试前等待7分钟。
1.6.3 排他作业
从近期版本开始,JobExecutor确保同一个流程实例的作业永远不会并发执行。为什么这样?
为什么使用排他作业?
考虑下面的流程定义:
一个并行网关,之后是三个服务任务,都使用异步操作执行。其结果是在数据库中添加了三个作业。当作业储存在数据库后,就由JobExecutor处理。JobExecutor获取作业,并将其委托至工作线程的线程池,由它们实际执行作业。这意味着通过使用异步操作,可以将工作分发至线程池(在集群场景下,甚至会在集群中跨越多个线程池)。通常这都是好事。但也有其固有问题:一致性。考虑服务任务后的并行合并:当服务任务的执行完成时到达并行合并,并需要决定等待其他执行,还是需要继续向前。这意味着,对于每一个到达并行合并的分支,都需要选择继续执行,还是需要等待其他分支上的一个或多个其他执行。
为什么这是问题呢?这是因为服务任务配置为使用异步延续,有可能所有相应的作业都同时被作业执行器处理,并委托至不同的工作线程。结果是服务执行的事务,与到达并行合并的3个独立执行所在的事务会发生重叠。如果这样,每一个独立事务都“看不到”其他事物已经并发地到达了同样的并行合并,并因此判断自己需要等待其他事务。然而,如果每个事务都判断需要等待其他事务,在并行合并后不会有继续流程的事务,而流程实例也就会永远保持这个状态。
Flowable如何解决这个问题呢?Flowable使用乐观锁。每当需要基于数据进行判断,而数据可能不是最新值(因为其他事务可能在我们提交前修改了这个数据)时,我们确保会在每个事务中都增加同一个数据库记录行的版本号。这样,无论哪个事务第一个提交,都将成功,而其他的会抛出乐观锁异常并失败。这样就解决了上面流程中讨论的问题:如果多个执行并发到达并行合并,它们都判断需要等待,增加其父执行(流程实例)的版本号并尝试提交。无论哪个执行第一个提交,都可以成功提交,而其他的将会抛出乐观锁异常并失败。因为这些执行由作业触发,Flowable会在等待给定时间后,重试执行相同的作业,期望这一次通过这个同步的网关。
这是好的解决方案么?我们可以看到乐观锁使Flowable避免不一致状态。它确保了我们不会“在合并网关卡住”,也就是说:要么所有的执行都通过网关,要么数据库中的作业能确保可以重试通过它。然而,尽管这是一个持久化与一致性角度的完美解决方案,但从更高层次看,仍然不一定总是理想行为:
- Flowable只会为同一个作业重试一个固定的最大次数(默认配置为3次)。在这之后,作业仍然保存在数据库中,但不会再重试。这就需要手动操作来触发作业。
- 如果一个作业有非事务性的副作用,则副作用将不会由于事务失败而回滚。例如,如果"book concert tickets(预定音乐会门票)"服务与Flowable不在同一个事务中,则重试执行作业将预定多张票。
什么是排他作业?
排他作业不能与同一个流程实例中的其他排他作业同时执行。考虑上面展示的流程:如果我们将服务任务都声明为排他的,则JobExecutor将确保相关的作业都不会并发执行。相反,它将确保不论何时从特定流程实例中获取了排他作业,都会从同一个流程实例中获取所有其他的排他作业,并将它们委托至同一个工作线程。这保证了作业的顺序执行。
如何启用这个特性?从近期版本开始,排他作业成为默认配置。所有异步操作与定时器事件都默认为排他的。另外,如果希望作业成为非排他的,可以使用flowable:exclusive="false"
配置。例如,下面的服务任务是异步,但非排他的。
<serviceTask id="service" flowable:expression="${myService.performBooking(hotel, dates)}"
flowable:async="true" flowable:exclusive="false" />
这是好的解决方案么?有很多人都在问这个问题。他们的顾虑是,这将阻止并行操作,因此会有性能问题。但也需要考虑以下两点:
- 如果你是专家,并且知道你在做什么(并理解“为什么排他作业?”章节的内容),可以关掉排他。否则,对大多数用户来说,异步操作与定时器能够正常工作才更重要。
- 事实上不会有性能问题。只有在重负载下才会有性能问题。重负载意味着作业执行器的所有的工作线程都一直忙碌。对于排他作业,Flowable会简单的根据负载不同进行分配。排他作业意味着同一个流程实例的作业都将在同一个线程中顺序执行。但是请想一下:我们有不止一个流程实例。其他流程实例的作业将被委托至其他线程,并与本实例的作业并发执行。也就是说Flowable不会并发执行同一个流程实例的排他作业,但会并发执行多个实例。从总吞吐量角度来看,可以期望大多数场景下都可以保证实例很快地完成。此外,执行同一个流程实例中下一个作业所需的数据,已经缓存在同一个执行集群节点中。如果作业与节点没有关联关系,则可能需要重新从数据库中获取数据。
1.7 流程启动认证
默认情况下,任何人都可以启动已部署流程定义的新流程实例。可以使用流程启动认证功能定义用户与组,让Web客户端可以选择性的限制能够启动新流程实例的用户。请注意Flowable引擎不会用任何方式验证认证定义。这个功能只是为了开发人员可以简化Web客户端认证规则的实现。语法与为用户任务指派用户的语法类似:可以使用flowable:potentialStarter标签,将用户或组指派为流程的潜在启动者。这里有一个例子:
<process id="potentialStarter">
<extensionElements>
<flowable:potentialStarter>
<resourceAssignmentExpression>
<formalExpression>group2, group(group3), user(user3)</formalExpression>
</resourceAssignmentExpression>
</flowable:potentialStarter>
</extensionElements>
<startEvent id="theStart"/>
...
在上面的XML中,user(user3)直接引用用户user3,而group(group3)引用组group3。不显式标明的话,默认为组。也可以使用标签提供的flowable:candidateStarterUsers与flowable:candidateStarterGroups的属性。这里有一个例子:
<process id="potentialStarter" flowable:candidateStarterUsers="user1, user2"
flowable:candidateStarterGroups="group1">
...
这些属性可以同时使用。
在流程启动认证定义后,开发者可以使用下列方法获取该认证定义。 这段代码获取可以由给定用户启动的流程定义列表:
processDefinitions = repositoryService.createProcessDefinitionQuery().startableByUser("userxxx").list();
也可以获取给定流程定义中,所有定义为潜在启动者的身份关联
identityLinks = repositoryService.getIdentityLinksForProcessDefinition("processDefinitionId");
下面的例子展示了如何获取能够启动给定流程的用户列表:
List<User> authorizedUsers = identityService().createUserQuery()
.potentialStarter("processDefinitionId")
.list();
用完全相同的方法,可以获取配置为给定流程定义的潜在启动者的组列表:
List<Group> authorizedGroups = identityService().createGroupQuery()
.potentialStarter("processDefinitionId")
.list();
1.8 数据对象
BPMN提供了将数据对象定义为流程或子流程元素的一部分的可能性。根据BPMN规范,数据对象可以包含复杂的XML结构,并可以从XSD定义中引入。下列XSD类型为Flowable支持的第一批数据对象:
<dataObject id="dObj1" name="StringTest" itemSubjectRef="xsd:string"/>
<dataObject id="dObj2" name="BooleanTest" itemSubjectRef="xsd:boolean"/>
<dataObject id="dObj3" name="DateTest" itemSubjectRef="xsd:datetime"/>
<dataObject id="dObj4" name="DoubleTest" itemSubjectRef="xsd:double"/>
<dataObject id="dObj5" name="IntegerTest" itemSubjectRef="xsd:int"/>
<dataObject id="dObj6" name="LongTest" itemSubjectRef="xsd:long"/>
数据对象定义使用name属性值作为新变量的名字,将其自动转换为流程变量。另外,Flowable也提供了为变量设置默认值的扩展元素。下面的BPMN代码片段示例:
<process id="dataObjectScope" name="Data Object Scope" isExecutable="true">
<dataObject id="dObj123" name="StringTest123" itemSubjectRef="xsd:string">
<extensionElements>
<flowable:value>Testing123</flowable:value>
</extensionElements>
</dataObject>
...