跳到主要内容

BPMN 2.0 结构

Doubao 中英对照 BPMN 2.0 Constructs

本章介绍 Flowable 支持的 BPMN 2.0 结构,以及对 BPMN 标准的自定义扩展。

自定义扩展​

BPMN 2.0 标准对所有相关方来说都是一件好事。最终用户不会因依赖专有解决方案而遭受厂商锁定。框架,尤其是像 Flowable 这样的开源框架,可以实现与大型厂商方案具有相同(而且往往实现得更好 ;-))功能的解决方案。得益于 BPMN 2.0 标准,从此类大型厂商方案迁移到 Flowable 可以是一条轻松顺畅的道路。

然而,标准的缺点在于,它始终是不同公司之间(往往还有不同愿景之间)多次讨论与妥协的结果。作为一名阅读流程定义 BPMN 2.0 XML 的开发人员,有时会觉得某些结构或做事方式非常繁琐。由于 Flowable 将开发便利性列为首要任务,我们引入了所谓的 'Flowable BPMN 扩展'。这些“扩展”是指不属于 BPMN 2.0 规范的、用于简化某些结构的新结构或新方式。

尽管 BPMN 2.0 规范明确说明其设计初衷就是支持自定义扩展,但我们确保:

  • 对于标准做事方式,始终必须存在一种简单的转换,作为此类自定义扩展的前提。因此当你决定使用某个自定义扩展时,不必担心没有回头路。
  • 在使用自定义扩展时,总是通过为新的 XML 元素、属性等添加 flowable: 命名空间前缀来明确标示。请注意,Flowable 引擎也支持 activiti: 命名空间前缀。

是否使用自定义扩展完全由你决定。有若干因素会影响这一决策(图形编辑器的使用、公司政策等)。我们提供这些扩展,只是因为我们认为标准中的某些点可以用更简单或更高效的方式来实现。欢迎随时就我们的扩展给我们反馈(无论正面还是负面),或提出自定义扩展的新想法。谁知道呢,也许有一天你的想法就会出现在规范里!

事件​

事件用于对流程生命周期中发生的某件事情建模。事件总是被可视化为一个圆圈。在 BPMN 2.0 中,存在两大主要事件类别:捕获 事件与 抛出 事件。

  • 捕获(Catching): 当流程执行到达该事件时,它会等待某个触发器发生。触发器的类型由内部图标或 XML 中的类型声明定义。捕获事件通过未填充的内部图标(只是白色)与抛出事件在视觉上区分开来。
  • 抛出(Throwing): 当流程执行到达该事件时,会触发一个触发器。触发器的类型由内部图标或 XML 中的类型声明定义。抛出事件通过填充为黑色的内部图标与捕获事件在视觉上区分开来。

事件定义​

事件定义定义了事件的语义。如果没有事件定义,事件“不会做任何特别的事情”。例如,一个没有事件定义的开始事件,无法指定究竟是什么启动了流程。如果我们向开始事件添加一个事件定义(例如定时事件定义),我们就声明了是哪种“类型”的事件启动了流程(在定时事件定义的情况下,即到达某个时间点这一事实)。

定时事件定义​

定时事件是由一个已定义的定时器触发的事件。它们可以用作开始事件、中间事件或边界事件。时间事件的行为取决于所使用的业务日历。每个定时事件都有一个默认业务日历,但业务日历也可以作为定时事件定义的一部分来指定。

<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 小时):

也可以在 timeCycle 上以可选属性的形式指定 endDate,或者在时间表达式的末尾指定,如下所示: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>

注意: 只有启用了异步执行器时定时器才会触发(必须在 flowable.cfg.xml 中将 asyncExecutorActivate 设置为 true,因为异步执行器默认是禁用的)。

错误事件定义​

重要提示: BPMN 错误与 Java 异常不是一回事。事实上,两者毫无共同之处。BPMN 错误事件是一种对业务异常建模的方式。Java 异常则以其自身特有的方式处理。

<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>

信号事件定义​

信号事件是引用某个具名信号的事件。信号是一种全局作用域的事件(广播语义),会被投递给所有活跃的处理器(等待中的流程实例/捕获信号事件)。

信号事件定义使用 signalEventDefinition 元素声明。属性 signalRef 引用一个声明为 definitions 根元素子元素的 signal 元素。下面是一个流程的片段,其中一个信号事件被中间事件抛出并捕获。

<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 标准的!)scope 属性:

<signal id="alertSignal" name="alert" flowable:scope="processInstance"/>

该属性的默认值为 "global"。

信号事件示例​

下面是两个独立流程使用信号进行通信的示例。当某个保险单被更新或变更时启动第一个流程。在变更经过人工参与者审核之后,抛出一个信号事件,表示保单已变更:

bpmn.signal.event.throw

现在,所有感兴趣的流程实例都可以捕获该事件。下面是一个订阅该事件的流程示例。

bpmn.signal.event.catch

注意: 重要的是要理解,信号事件会被广播给所有活跃的处理器。这意味着,在上面给出的示例中,捕获该信号的流程的所有实例都会收到该事件。在这个场景中,这正是我们想要的。然而,也存在广播行为并非预期的情况。请看下面这个流程:

bpmn.signal.event.warning.1

上面流程中描述的模式并不为 BPMN 所支持。其想法是:在执行“做某事”任务时抛出的错误,被边界错误事件捕获,通过信号抛出事件传播到并行的执行路径,然后中断“并行做某事”任务。到目前为止,Flowable 会按预期执行。信号会传播到捕获用的边界事件并中断该任务。**然而,由于信号的广播语义,它也会被传播给所有其他已订阅该信号事件的流程实例。**在这种情况下,这可能不是我们想要的。

注意: 信号事件不会与某个特定流程实例进行任何形式的关联(correlation)。相反,它会被广播给所有流程实例。如果你只需要将信号投递给某个特定流程实例,请手动进行关联,并使用 signalEventReceived(String signalName, String executionId),配合适当的查询机制。

Flowable 确实有一种方法可以解决这个问题:向信号事件添加 scope 属性,并将其设置为 processInstance。

消息事件定义​

消息事件是引用某个具名消息的事件。一条消息有一个名称和一个载荷(payload)。与信号不同,消息事件总是定向投递给单个接收者。

消息事件定义使用 messageEventDefinition 元素声明。属性 messageRef 引用一个声明为 definitions 根元素子元素的 message 元素。下面是一个流程的片段,其中声明了两个消息事件,并分别由一个开始事件和一个中间捕获消息事件引用。

<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 消息服务)队列/主题,或处理一个 Webservice 或 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();

由于对于某个特定消息订阅只能有一个流程定义,因此该查询总是返回零个或一个结果。如果流程定义被更新,
则只有最新版本的流程定义才拥有对该消息事件的订阅。

  • 对于中间捕获消息事件,消息事件订阅与
particular *execution*. Such message event subscriptions can be queried using a ExecutionQuery:
Execution execution = runtimeService.createExecutionQuery()
.messageEventSubscriptionName("paymentReceived")
.variableValueEquals("orderId", message.getOrderId())
.singleResult();

此类查询称为关联查询(correlation query),通常需要对流程有所了解(在本例中,即对于给定的 orderId 最多只有一个流程实例)。

消息事件示例​

下面是一个可以使用两种不同消息启动的流程示例:

bpmn.start.message.event.example.1

如果流程需要以不同方式对不同的开始事件作出反应、但最终又以统一的方式继续执行,这就很有用。

开始事件​

开始事件指示流程从何处开始。开始事件的类型(流程在消息到达时启动、在特定时间间隔启动等)定义了流程如何启动,在事件的可视化表示中显示为一个小图标。在 XML 表示中,类型由子元素的声明给出。

开始事件总是捕获型:从概念上讲,该事件(在任何时候)都在等待某个触发器发生。

在开始事件中,可以指定以下 Flowable 专有属性:

  • initiator:标识一个变量名,流程启动时会把已认证用户 ID 存入该变量。例如:
<startEvent id="request" flowable:initiator="initiator" />

必须在 try-finally 块中使用 IdentityService.setAuthenticatedUserId(String) 方法设置已认证用户,如下所示:

try {
identityService.setAuthenticatedUserId("bono");
runtimeService.startProcessInstanceByKey("someProcessKey");
} finally {
identityService.setAuthenticatedUserId(null);
}

这段代码已内置在 Flowable 应用中,因此可以与???结合使用。

无开始事件​

描述​

“无”开始事件在技术上意味着启动流程实例的触发器未指定。也就是说,引擎无法预知流程实例必须在何时启动。当通过 API 调用 startProcessInstanceByXXX 方法之一来启动流程实例时,使用无开始事件。

ProcessInstance processInstance = runtimeService.startProcessInstanceByXXX();

注意: 子流程总是有一个无开始事件。

图形表示​

无开始事件被可视化为一个没有内部图标的圆圈(换句话说,没有触发器类型)。

bpmn.none.start.event

XML 表示​

无开始事件的 XML 表示就是普通的开始事件声明,不带任何子元素(其他开始事件类型都有一个声明其类型的子元素)。

<startEvent id="start" name="my start event" />

无开始事件的自定义扩展​

formKey:引用一个表单定义,用户在启动新流程实例时必须填写该表单。更多信息请参阅表单章节。示例:

<startEvent id="request" flowable:formKey="request" />

定时开始事件​

描述​

定时开始事件用于在给定时间创建流程实例。它既可用于只应启动一次的流程,也可用于应在特定时间间隔启动的流程。

注意: 子流程不能有定时开始事件。

注意: 开始定时事件在流程部署后即被调度。无需调用 startProcessInstanceByXXX,不过调用启动流程方法也不受限制,并且会在调用 startProcessInstanceByXXX 时再额外启动一次流程。

注意: 当部署带开始定时事件的流程的新版本时,与先前定时器对应的作业将被移除。其原因是,通常并不希望继续自动启动旧版本流程的新流程实例。

图形表示​

图形表示​

定时开始事件被可视化为一个带有时钟内部图标的圆圈。

bpmn.clock.start.event

XML 表示​

定时开始事件的 XML 表示就是普通的开始事件声明,带上定时器定义子元素。配置细节请参阅定时器定义。

示例:流程将启动 4 次,间隔 5 分钟,从 2011 年 3 月 11 日 12:13 开始

<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>

消息开始事件​

描述​

消息开始事件可用于使用一个具名消息来启动流程实例。这实际上允许我们根据消息名称,从一组备选开始事件中选择正确的开始事件。

在部署带有一个或多个消息开始事件的流程定义时,需要注意以下事项:

  • 消息开始事件的名称在给定流程定义内必须唯一。一个流程定义不能有多个同名的消息开始事件。如果流程定义包含两个或多个引用同一消息的消息开始事件,或者两个或多个消息开始事件引用了具有相同消息名称的消息,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 是 messageEventDefinition 的 messageRef 属性所引用的 message 元素的 name 属性中给出的名称。在启动流程实例时需要注意以下事项:

  • 消息开始事件仅支持顶层流程。嵌入式子流程不支持消息开始事件。
  • 如果流程定义有多个消息开始事件,runtimeService.startProcessInstanceByMessage(...) 允许选择合适的开始事件。
  • 如果流程定义有多个消息开始事件和一个无开始事件,则 runtimeService.startProcessInstanceByKey(...) 和 runtimeService.startProcessInstanceById(...) 会使用无开始事件启动流程实例。
  • 如果流程定义有多个消息开始事件且没有无开始事件,则 runtimeService.startProcessInstanceByKey(...) 和 runtimeService.startProcessInstanceById(...) 会抛出异常。
  • 如果流程定义只有一个消息开始事件,则 runtimeService.startProcessInstanceByKey(...) 和 runtimeService.startProcessInstanceById(...) 会使用该消息开始事件启动一个新流程实例。
  • 如果流程是从调用活动(call activity)启动的,则消息开始事件只有在以下条件下才受支持:
-   in addition to the message start event(s), the process has a single none start event

- the process has a single message start event and no other start events.

图形表示​

消息开始事件被可视化为一个带消息事件符号的圆圈。该符号未填充,以表示捕获(接收)行为。

bpmn.start.message.event

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>

信号开始事件​

描述​

信号开始事件可用于使用一个具名信号来启动流程实例。该信号既可以在流程实例内部使用中间信号抛出事件“触发”,也可以通过 API(runtimeService.signalEventReceivedXXX 方法)触发。在这两种情况下,所有拥有同名信号开始事件的流程定义都会被启动。

请注意,在这两种情况下,都可以选择同步或异步地启动流程实例。

必须在 API 中传入的 signalName,就是 signalEventDefinition 的 signalRef 属性所引用的 signal 元素的 name 属性中给出的名称。

图形表示​

信号开始事件被可视化为一个带信号事件符号的圆圈。该符号未填充,以表示捕获(接收)行为。

bpmn.start.signal.event

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>

错误开始事件​

描述​

错误开始事件可用于触发一个事件子流程(Event Sub-Process)。错误开始事件不能用于启动流程实例。

错误开始事件总是中断型的。

图形表示​

错误开始事件被可视化为一个带错误事件符号的圆圈。该符号未填充,以表示捕获(接收)行为。

bpmn.start.error.event

XML 表示​

错误开始事件的 XML 表示就是普通的开始事件声明,带有一个 errorEventDefinition 子元素:

<startEvent id="messageStart" >
<errorEventDefinition errorRef="someError" />
</startEvent>

结束事件​

结束事件表示流程或子流程中某条路径的终点。结束事件总是抛出型。这意味着当流程执行到达结束事件时,会抛出一个结果。结果的类型由事件内部的黑色图标表示。在 XML 表示中,类型由子元素的声明给出。

无结束事件​

描述​

“无”结束事件意味着到达该事件时所抛出的结果未指定。因此,除了结束当前执行路径之外,引擎不会做任何额外的事情。

图形表示​

无结束事件被可视化为一个带粗边框、无内部图标的圆圈(没有结果类型)。

bpmn.none.end.event

XML 表示​

无结束事件的 XML 表示就是普通的结束事件声明,不带任何子元素(其他结束事件类型都有一个声明其类型的子元素)。

<endEvent id="end" name="my end event" />

错误结束事件​

描述​

当流程执行到达错误结束事件时,当前执行路径结束并抛出一个错误。该错误可以被匹配的中间边界错误事件捕获。如果找不到匹配的边界错误事件,则会抛出异常。

图形表示​

错误结束事件被可视化为一个典型的结束事件(带粗边框的圆圈),内部带有错误图标。错误图标为完全黑色,表示其抛出语义。

bpmn.error.end.event

XML 表示​

错误结束事件表示为一个结束事件,带有一个 errorEventDefinition 子元素。

<endEvent id="myErrorEndEvent">
<errorEventDefinition errorRef="myError" />
</endEvent>

errorRef 属性可以引用一个定义在流程外部的 error 元素:

<error id="myError" errorCode="123" />
...
<process id="myProcess">
...

error 的 errorCode 将用于查找匹配的捕获型边界错误事件。如果 errorRef 没有匹配到任何已定义的 error,则 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。

终止结束事件​

描述​

当到达终止结束事件时,当前流程实例或子流程将被终止。从概念上讲,当执行到达终止结束事件时,将确定第一个作用域(流程或子流程)并将其结束。请注意,在 BPMN 2.0 中,子流程可以是嵌入式子流程、调用活动、事件子流程或事务子流程。此规则普遍适用:例如当存在多实例调用活动或嵌入式子流程时,只有该实例会结束,其他实例和流程实例不受影响。

可以添加一个可选属性 terminateAll。当为 true 时,无论终止结束事件在流程定义中的位置如何,也无论是否处于子流程(甚至嵌套子流程)中,(根)流程实例都将被终止。

图形表示​

取消结束事件被可视化为一个典型的结束事件(带粗边框的圆圈),内部有一个全黑的圆。

bpmn.terminate.end.event

XML 表示​

终止结束事件表示为一个结束事件,带有一个 terminateEventDefinition 子元素。

请注意,terminateAll 属性是可选的(默认为 false)。

<endEvent id="myEndEvent >
<terminateEventDefinition flowable:terminateAll="true"></terminateEventDefinition>
</endEvent>

取消结束事件​

描述​

取消结束事件只能与 BPMN 事务子流程结合使用。当到达取消结束事件时,会抛出一个取消事件,该事件必须由取消边界事件捕获。然后取消边界事件取消该事务并触发补偿。

图形表示​

取消结束事件被可视化为一个典型的结束事件(带粗边框的圆圈),内部带有取消图标。取消图标为完全黑色,表示其抛出语义。

bpmn.cancel.end.event

XML 表示​

取消结束事件表示为一个结束事件,带有一个 cancelEventDefinition 子元素。

<endEvent id="myCancelEndEvent">
<cancelEventDefinition />
</endEvent>

边界事件​

边界事件是附着在某个活动上的捕获事件(边界事件绝不可能是抛出型)。这意味着在活动运行期间,该事件一直在监听某种类型的触发器。当事件被捕获时,活动被中断,并沿从该事件出发的顺序流继续执行。

所有边界事件的定义方式都相同:

<boundaryEvent id="myBoundaryEvent" attachedToRef="theActivity">
<XXXEventDefinition/>
</boundaryEvent>

边界事件通过以下内容定义

  • 一个唯一标识符(整个流程范围内)
  • 通过 attachedToRef 属性引用该事件所附着的活动。
    请注意,边界事件定义在与其所附着活动相同的层级上(换句话说,不要把边界事件包含在活动内部)。
  • 一个形式为 XXXEventDefinition 的 XML 子元素(例如 TimerEventDefinition、ErrorEventDefinition 等),用于定义边界事件的类型。更多细节请参阅各具体边界事件类型。

定时边界事件​

描述​

定时边界事件相当于一个秒表加闹钟。当执行到达该边界事件所附着的活动时,启动一个定时器。当定时器触发时(例如在指定间隔之后),活动被中断,并沿从边界事件出发的顺序流继续执行。

图形表示​

定时边界事件被可视化为一个典型的边界事件(位于边框上的圆圈),内部带有时钟图标。

bpmn.boundary.timer.event

XML 表示​

定时边界事件定义为一个常规边界事件。本例中的具体类型子元素是一个 timerEventDefinition 元素。

<boundaryEvent id="escalationTimer" cancelActivity="true" attachedToRef="firstLineSupport">
<timerEventDefinition>
<timeDuration>PT4H</timeDuration>
</timerEventDefinition>
</boundaryEvent>

定时器配置的细节请参阅定时事件定义。

在图形表示中,圆圈的线条为虚线,如上面示例所示:

bpmn.non.interrupting.boundary.timer.event

一个典型用例是:在一段时间后发送一封升级邮件,但不影响正常的流程流转。

中断型与非中断型定时器事件之间存在一个关键区别。非中断意味着原始活动不被中断,而是保持原样。中断行为是默认行为。在 XML 表示中,将 cancelActivity 属性设置为 false:

<boundaryEvent id="escalationTimer" cancelActivity="false" attachedToRef="firstLineSupport"/>

注意: 只有启用了异步执行器时边界定时器事件才会触发(必须在 flowable.cfg.xml 中将 asyncExecutorActivate 设置为 true,因为异步执行器默认是禁用的)。

边界事件的已知问题​

在使用任何类型的边界事件时,存在一个与并发相关的已知问题。目前,无法在一个边界事件上挂载多条出向顺序流。解决此问题的一种方案是:使用一条出向顺序流,通向一个并行网关。

bpmn.known.issue.boundary.event

错误边界事件​

描述​

位于活动边界上的中间捕获错误事件,简称边界错误事件,用于捕获在其所在活动作用域内抛出的错误。

边界错误事件最适合定义在嵌入式子流程或调用活动上,因为子流程会为其内部的所有活动创建一个作用域。错误由错误结束事件抛出。此类错误会沿着父作用域向上传播,直到找到一个定义了与该错误事件定义相匹配的边界错误事件的作用域为止。

当错误事件被捕获时,定义了该边界事件的活动会被销毁,同时也会销毁其中所有当前执行(并发活动、嵌套子流程等)。流程执行沿边界事件的出向顺序流继续。

图形表示​

边界错误事件被可视化为位于边界上的典型中间事件(圆圈内含一个小圆),内部带有错误图标。错误图标为白色,表示其捕获语义。

bpmn.boundary.error.event

XML 表示​

边界错误事件定义为一个典型的边界事件:

<boundaryEvent id="catchError" attachedToRef="mySubProcess">
<errorEventDefinition errorRef="myError"/>
</boundaryEvent>

与错误结束事件一样,errorRef 引用一个定义在流程元素外部的错误:

<error id="myError" errorCode="123" />
...
<process id="myProcess">
...

errorCode 用于匹配要捕获的错误:

  • 如果省略 errorRef,则边界错误事件将捕获任何错误事件,无论 error 的 errorCode 是什么。
  • 如果提供了 errorRef 且它引用了一个已存在的 error,则该边界事件将只捕获具有相同错误码的错误。
  • 如果提供了 errorRef,但 BPMN 2.0 文件中没有定义对应的 error,则errorRef 会被用作 errorCode(错误结束事件与此类似)。

示例​

下面的示例流程展示了如何使用错误结束事件。当 “Review profitability” 用户任务以“提供的信息不足”为由完成时,会抛出一个错误。当该错误在子流程边界上被捕获时,“Review sales lead” 子流程内所有活跃的活动都会被销毁(即使 “Review customer rating” 尚未完成),并创建 “Provide additional details” 用户任务。

bpmn.boundary.error.example

这个流程作为示例随演示环境一起发布。流程 XML 和单元测试可在 org.flowable.examples.bpmn.event.error 包中找到。

信号边界事件​

描述​

附着在活动边界上的中间捕获信号,简称边界信号事件,用于捕获与所引用信号定义同名的信号。

注意: 与其他事件(例如边界错误事件)不同,边界信号事件不仅捕获从其所附着作用域内抛出的信号事件。相反,信号事件具有全局作用域(广播语义),也就是说信号可以从任何地方抛出,甚至来自另一个流程实例。

注意: 与其他事件(例如错误事件)不同,信号在被捕获后并不会被消耗。如果你有两个活跃的信号边界事件捕获同一个信号事件,则两个边界事件都会被触发,即使它们属于不同的流程实例。

图形表示​

边界信号事件被可视化为位于边界上的典型中间事件(圆圈内含一个小圆),内部带有信号图标。信号图标为白色(未填充),表示其捕获语义。

bpmn.boundary.signal.event

XML 表示​

边界信号事件定义为一个典型的边界事件:

<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<signalEventDefinition signalRef="alertSignal"/>
</boundaryEvent>

示例​

请参阅信号事件定义一节。

消息边界事件​

描述​

附着在活动边界上的中间捕获消息,简称边界消息事件,用于捕获与所引用消息定义同名的消息。

图形表示​

边界消息事件被可视化为位于边界上的典型中间事件(圆圈内含一个小圆),内部带有消息图标。消息图标为白色(未填充),表示其捕获语义。

bpmn.boundary.message.event

请注意,边界消息事件既可以是中断型(右侧),也可以是非中断型(左侧)。

XML 表示​

边界消息事件定义为一个典型的边界事件:

<boundaryEvent id="boundary" attachedToRef="task" cancelActivity="true">
<messageEventDefinition messageRef="newCustomerMessage"/>
</boundaryEvent>

示例​

请参阅消息事件定义一节。

取消边界事件​

描述​

附着在事务子流程边界上的中间捕获取消事件,简称边界取消事件,在事务被取消时触发。当取消边界事件被触发时,它首先中断当前作用域内所有活跃的执行。接着,它对事务作用域内所有活跃的补偿边界事件启动补偿。补偿是同步执行的,换句话说,边界事件会等待补偿完成后再离开事务。补偿完成后,通过从取消边界事件出发的任意顺序流离开事务子流程。

注意: 一个事务子流程只允许有一个取消边界事件。

注意: 如果事务子流程包含嵌套子流程,则只有成功完成的子流程才会触发补偿。

注意: 如果将取消边界事件放置在具有多实例特性的事务子流程上,那么当任一实例触发取消时,该边界事件会取消所有实例。

图形表示​

取消边界事件被可视化为位于边界上的典型中间事件(圆圈内含一个小圆),内部带有取消图标。取消图标为白色(未填充),表示其捕获语义。

bpmn.boundary.cancel.event

XML 表示​

取消边界事件定义为一个典型的边界事件:

<boundaryEvent id="boundary" attachedToRef="transaction" >
<cancelEventDefinition />
</boundaryEvent>

由于取消边界事件总是中断型的,因此不需要 cancelActivity 属性。

补偿边界事件​

描述​

附着在活动边界上的中间捕获补偿事件,简称补偿边界事件,可用于将一个补偿处理器附着到某个活动上。

补偿边界事件必须通过一条有向关联引用唯一一个补偿处理器。

补偿边界事件的激活策略与其他边界事件不同。其他边界事件(例如信号边界事件)在其所附着的活动启动时被激活,活动结束时被停用,相应的事件订阅被取消。补偿边界事件则不同。补偿边界事件在其所附着的活动成功完成时被激活。此时,会创建对补偿事件的相应订阅。该订阅在补偿事件被触发时或相应流程实例结束时被移除。由此可得:

  • 当补偿被触发时,与补偿边界事件关联的补偿处理器被调用的次数,等于其所附着活动成功完成的次数。
  • 如果补偿边界事件附着在具有多实例特性的活动上,则会为每个实例创建一个补偿事件订阅。
  • 如果补偿边界事件附着在某个循环内的活动上,则每次执行该活动时都会创建一个补偿事件订阅。
  • 如果流程实例结束,则对补偿事件的订阅会被取消。

注意: 嵌入式子流程不支持补偿边界事件。

图形表示​

补偿边界事件被可视化为位于边界上的典型中间事件(圆圈内含一个小圆),内部带有补偿图标。补偿图标为白色(未填充),表示其捕获语义。除补偿边界事件外,下图还展示了通过单向关联与该边界事件关联的补偿处理器:

bpmn.boundary.compensation.event

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 属性。

中间捕获事件​

所有中间捕获事件的定义方式都相同:

<intermediateCatchEvent id="myIntermediateCatchEvent" >
<XXXEventDefinition/>
</intermediateCatchEvent>

中间捕获事件通过以下内容定义:

  • 一个唯一标识符(整个流程范围内)
  • 一个形式为 XXXEventDefinition 的 XML 子元素(例如 TimerEventDefinition),用于定义中间捕获事件的类型。更多细节请参阅各具体捕获事件类型。

中间捕获定时事件​

描述​

中间定时器事件相当于一个秒表。当执行到达捕获事件活动时,启动一个定时器。当定时器触发时(例如在指定间隔之后),沿从该中间定时器事件出发的顺序流继续执行。

图形表示​

中间定时器事件被可视化为一个中间捕获事件,内部带有时钟图标。

bpmn.intermediate.timer.event

XML 表示​

中间定时器事件定义为一个中间捕获事件。本例中的具体类型子元素是一个 timerEventDefinition 元素。

<intermediateCatchEvent id="timer">
<timerEventDefinition>
<timeDuration>PT5M</timeDuration>
</timerEventDefinition>
</intermediateCatchEvent>

配置细节请参阅定时事件定义。

中间捕获信号事件​

描述​

中间捕获信号事件用于捕获与所引用信号定义同名的信号。

注意: 与其他事件(例如错误事件)不同,信号在被捕获后并不会被消耗。如果你有两个活跃的信号边界事件捕获同一个信号事件,则两个边界事件都会被触发,即使它们属于不同的流程实例。

图形表示​

中间信号捕获事件被可视化为一个典型的中间事件(圆圈内含一个小圆),内部带有信号图标。信号图标为白色(未填充),表示其捕获语义。

bpmn.intermediate.signal.catch.event

XML 表示​

中间信号事件定义为一个中间捕获事件。本例中的具体类型子元素是一个 signalEventDefinition 元素。

<intermediateCatchEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateCatchEvent>

示例​

请参阅信号事件定义一节。

中间捕获消息事件​

描述​

中间捕获消息事件用于捕获具有指定名称的消息。

图形表示​

中间捕获消息事件被可视化为一个典型的中间事件(圆圈内含一个小圆),内部带有消息图标。消息图标为白色(未填充),表示其捕获语义。

bpmn.intermediate.message.catch.event

XML 表示​

中间消息事件定义为一个中间捕获事件。本例中的具体类型子元素是一个 messageEventDefinition 元素。

<intermediateCatchEvent id="message">
<messageEventDefinition signalRef="newCustomerMessage" />
</intermediateCatchEvent>

示例​

请参阅消息事件定义一节。

中间抛出事件​

所有中间抛出事件的定义方式都相同:

<intermediateThrowEvent id="myIntermediateThrowEvent" >
<XXXEventDefinition/>
</intermediateThrowEvent>

中间抛出事件通过以下内容定义:

  • 一个唯一标识符(整个流程范围内)
  • 一个形式为 XXXEventDefinition 的 XML 子元素(例如 signalEventDefinition),用于定义中间抛出事件的类型。更多细节请参阅各具体抛出事件类型。

中间无抛出事件​

下面的流程图展示了中间无事件的一个简单示例,它常用于表示流程中达到的某种状态。

bpmn.intermediate.none.event

通过添加一个执行监听器,这里可以成为监控某些 KPI 的良好挂钩点。

<intermediateThrowEvent id="noneEvent">
<extensionElements>
<flowable:executionListener class="org.flowable.engine.test.bpmn.event.IntermediateNoneEventTest$MyExecutionListener" event="start" />
</extensionElements>
</intermediateThrowEvent>

在这里你可以加入自己的代码,比如向你的 BAM 工具或 DWH 发送某个事件。在这种情况下,引擎本身不做任何事情,只是顺次通过。

中间抛出信号事件​

描述​

中间抛出信号事件为某个已定义的信号抛出一个信号事件。

在 Flowable 中,信号会被广播给所有活跃的处理器(换句话说,所有捕获信号事件)。信号可以同步或异步地发布。

  • 在默认配置下,信号是同步投递的。这意味着抛出信号的流程实例会等待,直到信号被投递给所有捕获该信号的流程实例。捕获流程实例与抛出流程实例在同一个事务中被通知,这意味着如果被通知的实例中有一个产生了技术性错误(抛出异常),则所有相关实例都会失败。
  • 信号也可以异步投递。在这种情况下,会确定在到达抛出信号事件时哪些处理器是活跃的。对于每个活跃处理器,都会存储一条异步通知消息(Job),并由 JobExecutor 投递。

图形表示​

中间信号抛出事件被可视化为一个典型的中间事件(圆圈内含一个小圆),内部带有信号图标。信号图标为黑色(已填充),表示其抛出语义。

bpmn.intermediate.signal.throw.event

XML 表示​

中间信号事件定义为一个中间抛出事件。本例中的具体类型子元素是一个 signalEventDefinition 元素。

<intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" />
</intermediateThrowEvent>

异步信号事件如下所示:

<intermediateThrowEvent id="signal">
<signalEventDefinition signalRef="newCustomerSignal" flowable:async="true" />
</intermediateThrowEvent>

示例​

请参阅信号事件定义一节。

中间抛出补偿事件​

描述​

中间抛出补偿事件可用于触发补偿。

触发补偿: 补偿既可以针对某个指定活动触发,也可以针对承载该补偿事件的作用域触发。补偿通过执行与某个活动关联的补偿处理器来完成。

  • 当为某个活动抛出补偿时,关联的补偿处理器被执行的次数,等于该活动成功完成的次数。
  • 如果为当前作用域抛出补偿,则当前作用域内的所有活动都会被补偿,包括并发分支上的活动。
  • 补偿是按层次触发的:如果要补偿的活动是一个子流程,则会为子流程内包含的所有活动触发补偿。如果子流程包含嵌套活动,则会递归抛出补偿。但是,补偿不会向上传播到流程的“上层”:如果在子流程内触发补偿,它不会传播到子流程作用域之外的活动。BPMN 规范规定,补偿是针对“同一子流程层级”的活动触发的。
  • 在 Flowable 中,补偿按执行的逆序进行。这意味着最后完成的活动最先被补偿,依此类推。
  • 中间抛出补偿事件可用于补偿成功完成的事务子流程。

注意: 如果在某个包含子流程的作用域内抛出补偿,且该子流程包含带有补偿处理器的活动,则只有在抛出补偿时子流程已成功完成,补偿才会传播到该子流程。如果子流程内嵌套的某些活动已完成且附着了补偿处理器,但包含这些活动的子流程尚未完成,则这些补偿处理器不会被执行。请看下面这个示例:

bpmn.throw.compensation.example1

在这个流程中,我们有两个并发执行:一个执行嵌入式子流程,另一个执行“charge credit card”活动。假设两个执行都已启动,第一个并发执行正在等待用户完成“review bookings”任务。第二个执行执行“charge credit card”活动时抛出了错误,导致“cancel reservations”事件触发补偿。此时并行子流程尚未完成,这意味着补偿事件不会传播到该子流程,因此“cancel hotel reservation”补偿处理器不会被执行。如果用户任务(以及嵌入式子流程)在执行“cancel reservations”之前完成,补偿就会传播到嵌入式子流程。

流程变量: 补偿嵌入式子流程时,用于执行补偿处理器的执行可以访问子流程的本地流程变量,且变量状态为子流程完成执行时的状态。为此,会对与该作用域执行(为执行子流程而创建的执行)关联的流程变量拍一个快照。由此可得几点影响:

  • 补偿处理器无法访问在子流程作用域内创建的并发执行中新增的变量。
  • 层次结构中更高级别执行所关联的流程变量(例如与流程实例执行关联的流程变量)不包含在快照中:补偿处理器可以访问这些流程变量,但看到的是抛出补偿时的状态。
  • 变量快照只针对嵌入式子流程拍摄,不针对其他活动。

当前限制:

  • waitForCompletion="false" 目前不受支持。当使用中间抛出补偿事件触发补偿时,只有在补偿成功完成后才会离开该事件。
  • 补偿本身目前由并发执行来执行。这些并发执行按被补偿活动完成顺序的逆序启动。
  • 补偿不会传播到由调用活动派生的子流程实例。

图形表示​

中间补偿抛出事件被可视化为一个典型的中间事件(圆圈内含一个小圆),内部带有补偿图标。补偿图标为黑色(已填充),表示其抛出语义。

bpmn.intermediate.compensation.throw.event

XML 表示​

中间补偿事件定义为一个中间抛出事件。本例中的具体类型子元素是一个 compensateEventDefinition 元素。

<intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition />
</intermediateThrowEvent>

此外,可以使用可选参数 activityRef 来触发对特定作用域或活动的补偿:

<intermediateThrowEvent id="throwCompensation">
<compensateEventDefinition activityRef="bookHotel" />
</intermediateThrowEvent>

顺序流​

描述​

顺序流是流程中两个元素之间的连接器。在流程执行过程中访问某个元素后,会沿其所有出向顺序流继续执行。这意味着 BPMN 2.0 的默认特性是并行的:两条出向顺序流会创建两条独立的并行执行路径。

图形表示​

顺序流被可视化为一条从源元素指向目标元素的箭头。箭头始终指向目标。

bpmn.sequence.flow

XML 表示​

顺序流需要一个在整个流程范围内唯一的 id,并引用一个已存在的 source(源)元素和 target(目标)元素。

<sequenceFlow id="flow1" sourceRef="theStart" targetRef="theTask" />

条件顺序流​

描述​

顺序流上可以定义一个条件。当离开某个 BPMN 2.0 活动时,默认行为是计算出向顺序流上的条件。当某个条件计算为 true 时,就选择该条出向顺序流。当以此方式选择了多条顺序流时,就会生成多个执行,并以并行方式继续流程。

注意: 上述规则适用于 BPMN 2.0 活动(及事件),但不适用于网关。网关会根据网关类型,以特定方式处理带条件的顺序流。

图形表示​

条件顺序流被可视化为一条普通顺序流,其开头带一个小菱形。条件表达式显示在顺序流旁边。

bpmn.conditional.sequence.flow

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 一起使用。有关这些的详细信息,请参阅表达式一节。所用表达式应解析为布尔值,否则在计算条件时会抛出异常。

  • 下面的示例以典型的 JavaBean 风格,通过 getter 引用某个流程变量的数据。
<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):

bpmn.uel expression.on.seq.flow

默认顺序流​

描述​

所有 BPMN 2.0 任务和网关都可以有一条默认顺序流。当且仅当其他顺序流都无法被选中时,该顺序流才会被选为该活动的出向顺序流。默认顺序流上的条件总是被忽略。

图形表示​

默认顺序流被可视化为一条普通顺序流,其开头带一个“斜杠”标记。

bpmn.default.sequence.flow

XML 表示​

某个活动的默认顺序流通过该活动上的 default 属性定义。下面的 XML 片段展示了一个排他网关示例,其默认顺序流为 flow 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>

对应的图形表示如下:

网关​

网关用于控制执行的流转(或如 BPMN 2.0 所述,控制执行的令牌)。网关能够消耗或生成令牌。

网关在图形上被可视化为一个菱形,内部带有一个图标。该图标显示网关的类型。

bpmn.gateway

排他网关​

描述​

排他网关(也称为 XOR 网关,更技术化的名称是 排他数据驱动网关)用于对流程中的一个决策建模。当执行到达该网关时,所有出向顺序流按其定义顺序依次被计算。第一个条件计算为 true 的顺序流(或未设置条件的顺序流,从概念上讲相当于在顺序流上定义了 'true')会被选中以继续流程。

请注意,在这种情况下出向顺序流的语义与 BPMN 2.0 中的一般情况不同。一般情况下,所有条件计算为 true 的顺序流都会被选中并以并行方式继续;而使用排他网关时只选择一条顺序流。如果有多条顺序流的条件都计算为 true,则选择 XML 中定义的第一条(且仅这一条!)来继续流程。如果没有任何顺序流可选,则会抛出异常。

图形表示​

排他网关被可视化为一个典型网关(菱形),内部带一个 'X' 图标,表示 XOR 语义。请注意,内部没有图标的网关默认就是排他网关。BPMN 2.0 规范不允许在同一个流程定义中同时使用带 X 和不带 X 的菱形。

bpmn.exclusive.gateway.notation

XML 表示​

排他网关的 XML 表示很直接:一行定义网关,条件表达式定义在出向顺序流上。这些表达式可用的选项请参阅条件顺序流一节。

以下面这个模型为例:

bpmn.exclusive.gateway

其 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>

并行网关​

描述​

网关也可用于对流程中的并发性建模。在流程模型中引入并发性最直接的网关就是并行网关,它允许你分叉为多条执行路径,或汇聚多条入向执行路径。

并行网关的功能基于入向和出向顺序流:

  • 分叉(fork): 所有出向顺序流被并行地沿续,为每条顺序流创建一个并发执行。
  • 汇聚(join): 到达并行网关的所有并发执行都在网关处等待,直到每条入向顺序流都有一个执行到达为止。然后流程穿过该汇聚网关继续。

请注意,如果同一个并行网关有多个入向和出向顺序流,则它可以同时具有分叉和汇聚行为。在这种情况下,网关会先汇聚所有入向顺序流,然后再分裂为多条并发执行路径。

与其他网关类型的一个重要区别是:并行网关不计算条件。如果连接到并行网关的顺序流上定义了条件,这些条件会被直接忽略。

图形表示​

并行网关被可视化为一个网关(菱形),内部带一个 '加号' 符号,表示 'AND' 语义。

bpmn.parallel.gateway

XML 表示​

定义一个并行网关只需一行 XML:

<parallelGateway id="myParallelGateway" />

实际行为(分叉、汇聚或两者兼有)由连接到并行网关的顺序流决定。

例如,上面的模型归结为下面的 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());

当这两个任务完成后,第二个并行网关会汇聚这两条执行;由于只有一条出向顺序流,不会创建并发执行路径,只有 Archive Order 任务处于活跃状态。

请注意,并行网关不需要“平衡”(即对应的并行网关入/出顺序流数量不必匹配)。并行网关只会等待所有入向顺序流,并为每条出向顺序流创建一条并发执行路径,不受流程模型中其他结构的影响。因此,下面这个流程在 BPMN 2.0 中是合法的:

bpmn.unbalanced.parallel.gateway

包容网关​

描述​

包容网关可以看作是排他网关与并行网关的组合。与排他网关一样,你可以在出向顺序流上定义条件,包容网关会计算这些条件。但主要区别在于:包容网关可以像并行网关那样,同时走多条顺序流。

包容网关的功能基于入向和出向顺序流:

  • 分叉(fork): 计算所有出向顺序流的条件,对于条件计算为 true 的顺序流,并行地沿续这些流,为每条顺序流创建一个并发执行。
  • 汇聚(join): 到达包容网关的所有并发执行都在网关处等待,直到每条带有流程令牌的入向顺序流都有一个执行到达为止。这是与并行网关的一个重要区别。换句话说,包容网关只会等待那些将要被执行的入向顺序流。汇聚之后,流程穿过该汇聚包容网关继续。

请注意,如果同一个包容网关有多个入向和出向顺序流,则它可以同时具有分叉和汇聚行为。在这种情况下,网关会先汇聚所有带有流程令牌的入向顺序流,然后再为那些条件计算为 true 的出向顺序流分裂为多条并发执行路径。

图形表示​

包容网关被可视化为一个网关(菱形),内部带一个 '圆圈' 符号。

bpmn.inclusive.gateway

XML 表示​

定义一个包容网关只需一行 XML:

<inclusiveGateway id="myInclusiveGateway" />

实际行为(分叉、汇聚或两者兼有)由连接到包容网关的顺序流决定。

例如,上面的模型归结为下面的 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());

当这个任务完成后,第二个包容网关会汇聚这两条执行;由于只有一条出向顺序流,不会创建并发执行路径,只有 Archive Order 任务处于活跃状态。

请注意,包容网关不需要“平衡”(即对应的包容网关入/出顺序流数量不必匹配)。包容网关只会等待所有入向顺序流,并为每条出向顺序流创建一条并发执行路径,不受流程模型中其他结构的影响。

基于事件的网关​

描述​

基于事件的网关(Event-based Gateway)提供了一种基于事件来做决策的方式。该网关的每条出向顺序流都必须连接到一个中间捕获事件。当流程执行到达基于事件的网关时,该网关表现为一个等待状态:执行被挂起。此外,还会为每条出向顺序流创建一个事件订阅。

请注意,从基于事件的网关出发的顺序流与普通顺序流不同。这些顺序流实际上从不会被“执行”。相反,它们用于让流程引擎判断到达基于事件网关的执行需要订阅哪些事件。适用以下限制:

  • 基于事件的网关必须有两条或更多出向顺序流。
  • 基于事件的网关只能连接到 intermediateCatchEvent 类型的元素(Flowable 不支持基于事件的网关之后接接收任务)。
  • 连接到基于事件网关的 intermediateCatchEvent 必须只有一条入向顺序流。

图形表示​

基于事件的网关被可视化为与其他 BPMN 网关一样的菱形,内部带一个特殊图标。

bpmn.event.based.gateway.notation

XML 表示​

用于定义基于事件网关的 XML 元素是 eventBasedGateway。

示例​

下面这个流程是一个使用基于事件网关的流程示例。当执行到达基于事件的网关时,流程执行被挂起。此外,流程实例订阅 alert 信号事件,并创建一个在 10 分钟后触发的定时器。这实际上使得流程引擎等待一个信号事件长达十分钟。如果信号在 10 分钟内发生,则定时器被取消,执行在信号之后继续。如果信号未被触发,则执行在定时器之后继续,信号订阅被取消。

bpmn.event.based.gateway.example

<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>

任务​

用户任务​

描述​

“用户任务”用于对需要由人来完成的工作建模。当流程执行到达这样的用户任务时,会在被分配到该任务的任何用户或组的任务列表中创建一个新任务。

图形表示​

用户任务被可视化为一个典型任务(圆角矩形),左上角带一个小用户图标。

bpmn.user.task

XML 表示​

用户任务在 XML 中定义如下。id 属性是必需的,name 属性是可选的。

<userTask id="theTask" name="Important task" />

用户任务也可以有描述。事实上,任何 BPMN 2.0 元素都可以有描述。描述通过添加 documentation 元素来定义。

<userTask id="theTask" name="Schedule meeting" >
<documentation>
Schedule an engineering meeting for next week with the new hire.
</documentation>

描述文本可以用标准的 Java 方式从任务中获取:

task.getDescription()

截止日期​

每个任务都有一个字段,表示该任务的截止日期。可以使用 Query API 查询在给定日期当天、之前或之后到期的任务。

有一种活动扩展,允许你在任务定义中指定一个表达式,以便在任务创建时设置其初始截止日期。该表达式应始终解析为 java.util.Date、java.util.String(ISO8601 格式)、ISO8601 时间时长(例如 PT50M)或 null。例如,你可以使用此前流程某个表单中输入的日期,或此前某个服务任务中计算出的日期。如果使用时间时长,则截止日期会根据当前时间计算并加上给定的时长。例如,当使用“PT30M”作为 dueDate 时,任务将在从现在起 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>

任务只能分配一个用户作为人工执行者。在 Flowable 术语中,这个用户称为办理人(assignee)。有办理人的任务不会出现在其他人的任务列表中,而是出现在该办理人的个人任务列表中。

直接分配给用户的任务可以通过 TaskService 如下获取:

List<Task> tasks = taskService.createTaskQuery().taskAssignee("kermit").list();

任务也可以放入人员的候选任务列表。在这种情况下,必须使用 potentialOwner 结构。其用法与 humanPerformer 结构类似。请注意,有必要在 formal expression 中为每个元素指明它是用户还是组(引擎无法猜测)。

<process >

...

<userTask id='theTask' name='important task' >
<potentialOwner>
<resourceAssignmentExpression>
<formalExpression>user(kermit), group(management)</formalExpression>
</resourceAssignmentExpression>
</potentialOwner>
</userTask>

使用 potential owner 结构定义的任务可以如下获取(或类似地使用 TaskQuery,与有办理人的任务相同):

 List<Task> tasks = taskService.createTaskQuery().taskCandidateUser("kermit");

这将获取所有 kermit 作为候选用户的任务,换句话说,formal expression 中包含 user(kermit) 的任务。这也会获取所有分配给某个组(而 kermit 是该组成员)的任务(例如 group(management),如果 kermit 是该组成员且使用了 Flowable 身份组件)。用户的组在运行时解析,可以通过IdentityService进行管理。

如果未指明给定文本字符串是用户还是组,引擎默认为组。下面这种写法与声明 group(accountancy) 等价。

<formalExpression>accountancy</formalExpression>

任务分配的 Flowable 扩展​

显然,对于分配并不复杂的用例来说,用户和组的分配方式相当繁琐。为避免这些复杂性,可以在用户任务上使用自定义扩展。

  • assignee 属性: 该自定义扩展允许将给定用户直接分配给某个任务。
<userTask id="theTask" name="my task" flowable:assignee="kermit" />

这与使用上面定义的 humanPerformer 结构完全相同。

  • candidateUsers 属性: 该自定义扩展使给定用户成为某个任务的候选者。
<userTask id="theTask" name="my task" flowable:candidateUsers="kermit, gonzo" />

这与使用上面定义的 potentialOwner 结构完全相同。请注意,与 potential owner 结构不同,这里无需使用 user(kermit) 声明,因为该属性只能用于用户。

  • candidateGroups 属性: 该自定义扩展使给定组成为某个任务的候选者。
<userTask id="theTask" name="my task" flowable:candidateGroups="management, accountancy" />

这与使用上面定义的 potentialOwner 结构完全相同。请注意,与 potential owner 结构不同,这里无需使用 group(management) 声明,因为该属性只能用于组。

  • candidateUsers 和 candidateGroups 可以同时定义在同一个用户任务上。

注意:尽管 Flowable 提供了一个身份管理组件(通过IdentityService暴露),但不会校验所提供的用户是否为身份组件所知。这样做是为了让 Flowable 在嵌入应用时能够与现有的身份管理方案集成。

自定义身份链接类型​

BPMN 标准支持单个已分配用户(即 humanPerformer),或一组构成 potentialOwners 候选池的用户,如用户分配中所定义。此外,Flowable 为用户任务定义了扩展属性元素,可表示任务的办理人(assignee)或候选办理人。

支持的 Flowable 身份链接类型有:

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 示例中的授权身份是 **user(用户)**和 group(组)。如上一节所述,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);
}

These are populated at runtime by the UserTaskActivityBehavior handleAssignments method.

Finally, the IdentityLinkType class must be extended to support the custom identity link types:

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>

传给 TaskListener 实现的 DelegateTask 可以设置办理人和候选用户/组:

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 时,可以使用上一节所述的自定义分配属性,并使用一个带有表达式的任务监听器来监听任务的 create 事件,从而委托给某个 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");
}

}

脚本任务​

描述​

脚本任务是一种自动活动。当流程执行到达脚本任务时,会执行相应的脚本。

属性:

  • name:任务属性,表示任务的名称
  • type:任务属性,其值必须为 "script",用于指示任务的类型
  • scriptFormat:扩展属性,表示脚本的语言(例如 javascript、groovy)
  • script:要执行的脚本,在一个名为 "script" 的字段元素中以字符串形式定义
  • autoStoreVariables:可选的任务属性标志(默认:false),表示脚本中定义的变量是否会存入 Execution 上下文(见下方注释)
  • resultVariableName:可选的任务属性;若存在,则会在 Execution 上下文中以指定名称保存一个变量,存放脚本的求值结果(见下方注释)

图形表示​

脚本任务被可视化为一个典型的 BPMN 2.0 任务(圆角矩形),矩形左上角带一个小“脚本”图标。

bpmn.scripttask

XML 表示​

脚本任务通过指定 script 和 scriptFormat 来定义。

<scriptTask id="theScriptTask" name="Execute script" scriptFormat="groovy">
<script>
sum = 0
for ( i in inputArray ) {
sum += i
}
</script>
</scriptTask>

scriptFormat 属性的值必须是一个与 JSR-223(Java 平台脚本规范)兼容的名称。默认情况下,每个 JDK 都内置 JavaScript,因此无需额外的 JAR 文件。如果你想使用另一种(与 JSR-223 兼容的)脚本引擎,只需将相应的 JAR 加入 classpath 并使用相应的名称即可。例如,Flowable 单元测试经常使用 Groovy,因为其语法与 Java 相似。

请注意,Groovy 脚本引擎随 groovy-jsr223 jar 一起提供。因此,必须添加以下依赖:

<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-jsr223</artifactId>
<version>4.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 作为脚本语言时,也可以使用“安全脚本(secure scripting)”。请参阅安全脚本一节。

Java 服务任务​

描述​

Java 服务任务用于调用一个外部 Java 类。

图形表示​

服务任务被可视化为一个圆角矩形,左上角带一个小齿轮图标。

bpmn.java.service.task

XML 表示​

声明如何调用 Java 逻辑有四种方式:

  • 指定一个实现了 JavaDelegate、FutureJavaDelegate 或 ActivityBehavior 的类
  • 计算一个解析为委托对象的表达式
  • 调用一个方法表达式
  • 计算一个值表达式

要指定一个在流程执行期间被调用的类,需要通过 '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 或 FutureJavaDelegate 接口的 Bean,例如定义在 Spring 容器中。

要指定一个应当被计算的 UEL 方法表达式,使用 flowable:expression 属性。

<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage()}" />

将在名为 printer 的对象上调用 printMessage 方法(无参数)。

也可以在表达式中为所使用的方法传递参数。

<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{printer.printMessage(execution, myVar)}" />

将在名为 printer 的对象上调用 printMessage 方法。传入的第一个参数是 DelegateExecution,它在表达式上下文中默认可用,名为 execution。传入的第二个参数是当前执行中名为 myVar 的变量的值。

注意:当 flowable:expression 中定义的方法执行耗时较长时,可以返回一个 CompletableFuture<?>。使用它时,其他并行流可以在同一时间继续执行。

要指定一个应当被计算的 UEL 值表达式,使用 flowable:expression 属性。

<serviceTask id="javaService"
name="My Java Service Task"
flowable:expression="#{split.ready}" />

将在名为 split 的 Bean 上调用属性 ready 的 getter 方法 getReady(无参数)。这些命名对象在执行的流程变量中解析,(如适用)也在 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)。这意味着该类不得使用任何成员变量,并且必须是线程安全的,因为它可能被不同线程同时执行。这也会影响字段注入的处理方式。

在流程定义中引用的类(通过 flowable:class)不会在部署时实例化。只有当流程执行第一次到达流程中使用该类的位置时,才会创建该类的实例。如果找不到该类,将抛出 FlowableException。这样设计的原因是,部署时的环境(更具体地说,classpath)往往与实际运行时环境不同。例如,当使用 ant 或在 Flowable 应用中通过业务归档上传来部署流程时,classpath 不会自动包含所引用的类。

如果执行耗时较长,可以把该工作委托到另一个线程上,同时继续执行并行活动。为此需要实现 org.flowable.engine.delegate.FutureJavaDelegate。这是一个更高级的接口,需要特别小心,以便在 DelegateExecution 上设置数据,并处理好事务边界。

public class LongRunningJavaDelegate implements FutureJavaDelegate<String> {

public CompletableFuture<String> execute(DelegateExecution execution, AsyncTaskInvoker taskInvoker) {
// This is running in the same transaction as the process instance and is still possible to set and extract data from the execution
String input = (String) execution.getVariable("input");
// The taskInvoker is a common invoker provided by Flowable that can be used to submit complex executions on a new thread.
// However, you don't have to use it, you can use your own custom ExecutorService or return a CompletableFuture from your own services.
return taskInvoker.submit(() -> {
// This is running on a new thread. The execution shouldn't be used here.
// There is also no transaction here. In case a new transaction is needed, then it should be managed by your own services
// Perform some complex logic that takes some time, e.g. invoking an external service
return "done";
});
}

public void afterExecution(DelegateExecution execution, String executionData) {
// This is running in the same transaction and thread as the process instance and data can be set on the execution
execution.setVariable("longRunningResult", executionData);
}
}

还有另外 2 个接口可用于简化你自己的实现:

  • org.flowable.engine.delegate.FlowableFutureJavaDelegate,它会使用默认(或已配置)的 AsyncTaskInvoker,从而简化逻辑。
  • org.flowable.engine.delegate.MapBasedFlowableFutureJavaDelegate,它进一步简化了输入和输出的传递,分别使用 ReadOnlyDelegateExecution 和 Map。

使用其他接口的同一实现如下:

public class LongRunningJavaDelegate implements FlowableFutureJavaDelegate<String, String> {


public String prepareExecutionData(DelegateExecution execution) {
// This is running in the same transaction and thread as the process instance.
// The execution can be used
return execution.getVariable("input");
}

public String execute(String inputData) {
// This is running on a new thread. The execution shouldn't be used here.
// There is also no transaction here. In case a new transaction is needed, then it should be managed by your own services
// Perform some complex logic that takes some time, e.g. invoking an external service
return "done";
}

public void afterExecution(DelegateExecution execution, String executionData) {
// This is running in the same transaction and thread as the process instance and data can be set on the execution
execution.setVariable("longRunningResult", executionData);
}
}
public class LongRunningJavaDelegate implements MapBasedFlowableFutureJavaDelegate {

public Map<String, Object> execute(ReadOnlyDelegateExecution execution) {
// The execution is a read only snapshot of the delegate execution
// This is running on a new thread. The execution shouldn't be used here.
// There is also no transaction here. In case a new transaction is needed, then it should be managed by your own services
// Perform some complex logic that takes some time, e.g. invoking an external service
Map<String, Object> result = new HashMap<>();
result.put("longRunningResult", "done");
// All the values from the returned map will be set on the execution
return result;
}
}

注意:适用于 JavaDelegate 的相同规则和逻辑同样适用于 FutureJavaDelegate。请记住,在使用字段表达式注入时,表达式的计算应只在执行完成之前或之后进行(在与流程实例相同的线程上)。

[INTERNAL: non-public implementation classes] 也可以提供一个实现了 org.flowable.engine.impl.delegate.ActivityBehavior 接口的类。这样的实现可以访问更强大的引擎功能,例如影响流程的控制流。但请注意,这并不是一种好的做法,应尽量避免。因此,建议仅在高级用例、且你确切知道自己在做什么时才使用 ActivityBehavior 接口。

字段注入​

可以把值注入到委托类的字段中。支持以下几种注入类型:

  • 固定字符串值
  • 表达式

如果可用,值会通过委托类上的 public setter 方法注入,遵循 Java Bean 命名约定(例如,字段 firstName 有 setter setFirstName(...))。如果该字段没有 setter,则会在委托对象上设置该私有成员的值。某些环境中的 SecurityManager 不允许修改私有字段,因此最好为希望注入的字段暴露一个 public setter 方法。

无论流程定义中声明的值是什么类型,注入目标上的 setter/私有字段类型都应始终为 org.flowable.engine.delegate.Expression。表达式解析后,可以再转换为适当的类型。

使用 'flowable:class' 属性时支持字段注入。使用 flowable:delegateExpression 属性时也可以进行字段注入,但此时会有关于线程安全的特殊规则(见下一节)。

下面的代码片段展示了如何把一个常量值注入到类中声明的字段。请注意,我们需要在实际的字段注入声明之前声明一个 'extensionElements' XML 元素,这是 BPMN 2.0 XML Schema 的要求。

<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 定义的 Bean(如果使用了 Spring)。如服务任务实现中所述,当使用 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 类的一个实现,而我们运行在一个为每个 Bean 创建单例实例的环境中(例如 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 委托,改用表达式,并通过方法参数把所需数据传给委托。
  • 每次解析委托表达式时都返回委托类的一个新实例。例如,使用 Spring 时,这意味着必须把 Bean 的作用域设置为 prototype(例如在委托类上加 @Scope(SCOPE_PROTOTYPE) 注解)。

从 Flowable v5.22 开始,可以通过设置 delegateExpressionFieldInjectionMode 属性的值(取值为 org.flowable.engine.imp.cfg.DelegateExpressionFieldInjectionMode 枚举中的一个值),以某种方式配置流程引擎,禁用在委托表达式上使用字段注入。

可选设置如下:

  • DISABLED:在使用委托表达式时完全禁用字段注入。不会尝试任何字段注入。在线程安全方面这是最安全的模式。
  • COMPATIBILITY:在该模式下,行为与 v5.21 之前完全一致:使用委托表达式时可以进行字段注入,如果委托类上未定义这些字段,则会抛出异常。当然,在线程安全方面这是最不安全的模式,但出于向后兼容可能有此必要;或者当委托表达式只在一组流程定义中的某一个任务上使用时(因此不会发生并发竞态条件),也可以安全使用。
  • MIXED:允许在使用 delegateExpressions 时注入,但当委托上未定义这些字段时不会抛出异常。这样可以混合行为:某些委托有注入(例如因为它们不是单例),某些没有。
  • 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 的作用域为 prototype,Spring 容器每次请求该 Bean 时都会返回一个新实例。

给定以下流程定义:

<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} 委托表达式。

先看 prototype 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,会得到 2,因为每次解析 ${prototypeDelegateExpressionBean} 时都会创建一个新实例。这里字段可以毫无问题地注入,我们可以看到这里有三个 Expression 成员字段。

而 singleton 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);
}

}

这里的 INSTANCE_COUNT 始终是 1,因为它是单例。这个委托中没有 Expression 成员字段。这之所以可行,是因为我们运行在 MIXED 模式。在 COMPATIBILITY 模式下,这会抛出异常,因为它期望成员字段存在。DISABLED 模式对这个 Bean 也可行,但它会禁止使用上面那个确实使用字段注入的 prototype Bean。

在这段委托代码中,使用了 org.flowable.engine.delegate.DelegateHelper 类,它提供了一些有用的工具方法,可在委托为单例时以线程安全的方式执行相同逻辑。它不是注入 Expression,而是通过 getFieldExpression 方法获取。这意味着,就服务任务 XML 而言,字段的定义与 singleton Bean 完全相同。看上面的 XML 片段可以发现,它们的定义是一样的,只是实现逻辑不同。

技术说明:getFieldExpression 会内省 BpmnModel,并在方法执行时动态创建 Expression,从而保证线程安全。

  • 对于 Flowable v5.x,DelegateHelper 不能用于 ExecutionListener 或 TaskListener(由于架构上的缺陷)。要让这些监听器的实例线程安全,请使用表达式,或确保每次解析委托表达式时都创建一个新实例。
  • 对于 Flowable V6.x,DelegateHelper 确实可以在 ExecutionListener 和 TaskListener 实现中使用。例如,在 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" />

在上面的示例中,服务执行的结果(即在名为 'myService' 的对象上调用 'doSomething()' 方法的返回值,该对象要么来自流程变量,要么是一个 Spring Bean)在服务执行完成后被设置到名为 'myVar' 的流程变量中。

注意:当方法返回 CompletableFuture<?> 时,future 的结果会被设置到结果变量中。

可触发​

一种常见模式是:通过发送 JMS 消息或发起 HTTP 调用来触发外部服务,并让流程实例进入等待状态。然后在某个时刻,外部服务返回响应,流程实例继续到下一个活动。使用默认 BPMN,你需要对一个服务任务和一个接收任务建模。但这会引入一些竞态条件,因为外部服务的响应可能在流程实例被持久化、接收任务变为活跃之前就已返回。为解决这个问题,Flowable 在服务任务上支持 triggerable 属性,它把单个服务任务转变为这样一个任务:先执行服务逻辑,然后等待外部触发,再继续到流程定义中的下一个活动。如果一个可触发服务任务还同时设置了 async 属性为 true,则会先持久化流程实例状态,然后在一个异步 Job 中执行服务任务逻辑。在 BPMN XML 中,一个异步可触发服务任务实现如下:

<serviceTask id="aTriggerableServiceTask"
flowable:expression="#{myService.doSomething()}"
flowable:triggerable="true"
flowable:async="true" />

当外部服务想再次触发等待中的流程实例时,可以同步或异步地进行。为防止乐观锁异常,异步触发是最佳方案。默认情况下异步 Job 是排他的,这意味着流程实例会被锁定,并保证该流程实例上没有其他活动会干扰触发逻辑。可以使用 RuntimeService 的 triggerAsync 方法以异步方式触发等待中的流程实例。当然,仍然可以使用 RuntimeService 的 trigger 方法以同步方式完成。

异常处理​

执行自定义逻辑时,往往需要捕获某些业务异常并在周围流程中处理它们。Flowable 提供了多种方式来做到这一点。

抛出 BPMN 错误​

可以从服务任务或脚本任务内的用户代码中抛出 BPMN 错误。为此,可以在 JavaDelegates、FutureJavaDelegates、脚本、表达式和委托表达式中抛出一个名为 BpmnError 的特殊 FlowableException。引擎会捕获该异常并将其转发给相应的错误处理器,例如边界错误事件或错误事件子流程。

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("var");

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 数据源)。例如,下面这段代码与上面那段效果相同,但这里 RuntimeService 是被注入的,而不是通过 org.flowable.engine.EngineServices 接口获取的。

@Component("startProcessInstanceDelegate")
public class StartProcessInstanceTestDelegateWithInjection {

@Autowired
private RuntimeService runtimeService;

public void startProcess() {
runtimeService.startProcessInstanceByKey("oneTaskProcess");
}

}

重要技术说明: 由于服务调用是作为当前事务的一部分进行的,因此在服务任务执行之前产生或修改的任何数据都尚未 flush 到数据库。所有 API 调用都基于数据库数据工作,这意味着这些未提交的变更在服务任务的 API 调用内是不可见的。

Web 服务任务​

描述​

Web 服务任务用于同步调用一个外部 Web 服务。

图形表示​

Web 服务任务的可视方式与 Java 服务任务相同。

bpmn.web.service.task

XML 表示​

要使用 Web 服务,我们需要导入它的操作和复杂类型。可以使用指向该 Web 服务 WSDL 的 import 标签自动完成:

<import importType="http://schemas.xmlsoap.org/wsdl/"
location="http://localhost:63081/counter?wsdl"
namespace="http://webservice.flowable.org/" />

上面的声明告诉 Flowable 导入这些定义,但它不会替你创建条目定义和消息。假设我们想调用一个名为 '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 接口和操作。基本上,我们定义一个 'interface' 和所需的 'operations'。每个操作都复用前面定义的 'in' 和 'out' 消息。例如,下面的声明定义了 '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>

业务规则任务​

描述​

业务规则任务用于同步执行一条或多条规则。Flowable 使用 Drools Expert(Drools 规则引擎)来执行业务规则。目前,包含业务规则的 .drl 文件必须与定义业务规则任务以执行这些规则的流程定义一起部署。这意味着流程中使用的所有 .drl 文件都必须打包进流程 BAR 文件,与任务表单等一样。有关为 Drools Expert 创建业务规则的更多信息,请参阅 JBoss Drools 的 Drools 文档。

如果你想插入自己的规则任务实现,例如因为你想以不同方式使用 Drools,或者想使用完全不同的规则引擎,那么可以在 BusinessRuleTask 上使用 class 或 expression 属性,它的行为将与服务任务完全一样。

图形表示​

业务规则任务被可视化为带表格图标的样子。

bpmn.business.rule.task

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" />

In this case only rule1 and rule2 are executed.

在这种情况下,只会执行 rule1 和 rule2。

你也可以定义一个应从执行中排除的规则列表。

<businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
flowable:rules="rule1, rule2" exclude="true" />

在这种情况下,部署在与流程定义同一个 BAR 文件中的所有规则都会被执行,但 rule1 和 rule2 除外。

如前所述,另一种选择是自己接入 BusinessRuleTask 的实现:

<businessRuleTask id="businessRuleTask" flowable:class="${MyRuleServiceDelegate}" />

此时 BusinessRuleTask 的行为与 ServiceTask 完全一样,但仍保留 BusinessRuleTask 图标,以可视化表示我们在这里进行的是业务规则处理。

邮件任务​

Flowable 允许你用自动邮件服务任务增强业务流程,向一个或多个收件人发送电子邮件,并支持抄送、密送、HTML 内容等。请注意,邮件任务并不是 BPMN 2.0 规范中的“官方”任务(因此也没有专门的图标)。因此,在 Flowable 中,邮件任务是作为一种专用服务任务实现的。

邮件服务器配置​

Flowable 引擎通过一个具备 SMTP 能力的外部邮件服务器发送电子邮件。要真正发送邮件,引擎需要知道如何连接到邮件服务器。可以在 flowable.cfg.xml 配置文件中设置以下属性:

属性是否必需?描述
mailServerHost否邮件服务器的主机名(例如 mail.mycorp.com)。默认是 localhost
mailServerPort如果不在默认端口上则必需邮件服务器上 SMTP 通信的端口。默认是 25
mailServerForceTo否如果设置,发送邮件时将用它替换邮件任务中配置的 to、cc 和/或 bcc。
mailServerDefaultFrom否用户未提供时邮件发件人的默认电子邮件地址。默认是 flowable@localhost
mailServerUsername视你的服务器而定某些邮件服务器发送邮件时需要凭证。默认未设置。
mailServerPassword视你的服务器而定某些邮件服务器发送邮件时需要凭证。默认未设置。
mailServerUseSSL视你的服务器而定某些邮件服务器要求 SSL 通信。默认设为 false。
mailServerUseTLS视你的服务器而定某些邮件服务器(例如 gmail)要求 TLS 通信。默认设为 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 而不抛出异常时,使用指定名称的变量来存放失败消息

用法示例​

下面的 XML 片段展示了使用邮件任务的一个示例。

<serviceTask id="sendMail" flowable:type="mail">
<extensionElements>
<flowable:field name="from" stringValue="order-shipping@thecompany.com" />
<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>

Http 任务​

Http 任务允许发起 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.engine.impl.bpmn.http.DefaultBpmnHttpActivityDelegate 并重写你需要的方法。

任务定义中应设置字段 'httpActivityBehaviorClass'(该字段的默认值为 'org.flowable.engine.impl.bpmn.http.DefaultBpmnHttpActivityDelegate')。

http 客户端通过 FlowableHttpClient 抽象。使用 flowable-http 依赖时的默认实现基于 Apache Http Client。由于 Apache Http Client 可以从多方面定制,并非所有可能选项都用于 Http 客户端配置。还有一个基于 Spring WebClient 的实现,当 Apache Http Client 不在 classpath 上时会使用它。

要创建自定义客户端,请参阅 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>
</serviceTask>

Http 任务配置​

Http 任务通过字段注入进行配置。这些属性的值都可以包含 EL 表达式,在执行期间于运行时解析。可以设置以下属性:

属性是否必需?描述
requestMethod是请求方法(GET、POST、PUT、DELETE)。
requestUrl是请求 URL(示例 - http://flowable.org)。
requestHeaders否以换行分隔的 Http 请求头。示例 - Content-Type: application/json Authorization: Basic aGFRlc3Q=
requestBody否请求体 示例 - ${sampleBody}
requestTimeout否请求超时,单位毫秒(示例 - 5000)。默认是 '0',表示不超时。连接相关超时请参阅 Http Client 配置。
disallowRedirects否禁止 Http 重定向的标志。默认 false。(示例 - true)。
failStatusCodes否以逗号分隔的 Http 响应状态码列表,命中则请求失败并抛出 FlowableException。示例:400, 404, 500, 503 示例:400, 5XX
handleStatusCodes否以逗号分隔的状态码列表,命中则任务抛出 BpmnError,可由边界错误事件捕获。BpmnError 的错误码为 'HTTP<statuscode>'。例如 404 状态码对应错误码 'HTTP404'。只有当 'disallowRedirects' 字段也设置时才会抛出 3XX 状态码。当 'handleStatusCodes' 与 'failStatusCodes' 同时设置时,前者覆盖后者。示例:400, 404, 500, 503 示例:3XX, 4XX, 5XX
ignoreException否忽略异常的标志。异常被捕获,并以 '<taskId>ErrorMessage' 为名存为变量。
saveRequestVariables否保存请求变量的标志。默认只把与响应相关的变量存为变量。
saveResponseParameters否保存所有响应变量(包括 HTTP 状态、头等)的标志。默认只把响应体存为变量。
resultVariablePrefix否执行变量名的前缀。若未设置前缀,变量将以 '<taskId>FieldName' 为名保存。例如,对 id 为 'task7' 的任务,requestUrl 存为 'task7RequestUrl'。
saveResponseParametersTransient否若解析为 true,响应体变量(以及响应头、状态等,如果设置为存储)将被存为瞬时变量。
saveResponseVariableAsJson否若解析为 true,响应体值将被存为 JSON 变量而非 String。当 HTTP 服务返回 JSON、且你之后想用点号记法引用字段(例如 myResponse.user.name)时很有用。
parallelInSameTransaction-若解析为 true,HTTP 调用将在另一个线程上进行,从而可以并行执行更多 HTTP 任务。当使用并行网关且并行流中有多个 HTTP 任务时很有用。
httpActivityBehaviorClass否org.flowable.engine.impl.bpmn.http.DefaultBpmnHttpActivityDelegate 自定义扩展的全类名。

除上述字段外,根据 'saveResponseParameters' 标志,成功执行后还会把以下内容设为变量:

变量是否可选?描述
responseProtocol是Http 版本
responseReason是Http 响应原因短语。
responseStatusCode是Http 响应状态码(示例 - 200)。
responseHeaders是以换行分隔的 Http 响应头。示例 - Content-Type: application/json Content-Length: 777
responseBody是响应体(字符串形式),如果有。
errorMessage是被忽略的错误消息,如果有。

结果变量​

请记住,上面所有执行变量名都以 'resultVariablePrefix' 的求值结果为前缀。例如,响应状态码可在另一个活动中以 'task7ResponseStatusCode' 访问。这里的 'task7' 是该服务任务的 'id'。要覆盖此行为,请按需设置 'resultVariablePrefix'。

用法示例​

下面的 XML 片段展示了使用 Http 任务的一个示例。

<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。默认情况下,任何重定向/客户端/服务端错误 http 状态码都不被处理。

可以通过设置 'failStatusCodes' 和/或 'handleStatusCodes' 字段,把 Http 任务配置为处理这些异常和 http 状态。参阅 Http 任务配置。

'handleStatusCodes' 抛出的 BpmnError 应像普通 BPMN 异常一样处理,即配置相应的边界错误事件。下面是几个 http 任务异常处理与重试的示例。

在 '400' 和 5XX 时失败,异步 http 任务并用 failedJobRetryTimeCycle 重试​
<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' 作为 BpmnError 处理​
<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>
异常映射​

参阅异常映射。

Camel 任务​

Camel 任务允许你向 Camel 发送消息并从 Camel 接收消息,从而增强 Flowable 的集成能力。请注意,Camel 任务并不是 BPMN 2.0 规范中的“官方”任务(因此也没有专门的图标)。因此,在 Flowable 中,Camel 任务是作为一种专用服务任务实现的。另请注意,要使用 Camel 任务功能,必须在项目中包含 Flowable 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 调用示例​

与本示例相关的所有文件都在 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 端点的格式。它由三部分组成:

端点 URL 部分描述
flowable指向引擎端点
SimpleCamelCallProcess流程名称
simpleCall流程中 Camel 服务的名称

好了,我们的路由现在已正确配置并可被 Camel 访问。接下来是工作流部分。工作流如下:

<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>

Ping Pong 示例​

我们的示例虽然能运行,但 Camel 和 Flowable 之间并没有真正传输任何数据,因此价值不大。在这个示例中,我们尝试向 Camel 发送数据并从 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 行为(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("${exchangeProperty.input} World");
}

在这条路由中,字符串 "world" 被拼接到名为 "input" 的属性末尾,结果会被设置到消息体中。在 Java 服务任务中检查 "camelBody" 变量即可访问它,并复制到 "outputMap"。现在这个使用默认行为的示例已能运行,我们来看看还有哪些其他可能。在启动每条 Camel 路由时,流程实例 ID 会被复制到一个 Camel 属性中,该属性名为 "PROCESS_ID_PROPERTY"。它稍后用于关联流程实例与 Camel 路由,也可以在 Camel 路由中加以利用。

Flowable 开箱即用地提供了三种不同行为。可以在路由 URL 中通过特定短语覆盖行为。下面是在 URL 中覆盖已定义行为的示例:

from("flowable:asyncCamelProcess:serviceTaskAsync2?copyVariablesToProperties=true").

下表概览了三种可用的 Camel 行为:

行为URL 中描述
CamelBehaviorDefaultImplcopyVariablesToProperties把 Flowable 变量复制为 Camel 属性
CamelBehaviorCamelBodyImplcopyCamelBodyToBody只把名为 "camelBody" 的 Flowable 变量复制为 Camel 消息体
CamelBehaviorBodyAsMapImplcopyVariablesToBodyAsMap把所有 Flowable 变量以 map 形式复制为 Camel 消息体

上表描述 Flowable 变量如何传输到 Camel。下表描述 Camel 变量如何返回给 Flowable。这只能在路由 URL 中配置。

URL描述
默认如果 Camel 消息体是 map,把每个元素复制为一个 Flowable 变量;否则把整个 Camel 消息体复制为一个 "camelBody" Flowable 变量
copyVariablesFromProperties把 Camel 属性复制为同名 Flowable 变量
copyCamelBodyToBodyAsString与默认相同,但如果 camelBody 不是 map,先把它转为 String,再复制到 "camelBody"
copyVariablesFromHeader额外把 Camel 头复制为同名 Flowable 变量

把变量返回​

上面关于传递变量的说明只适用于变量传输的发起方,无论你是从 Camel 到 Flowable,还是从 Flowable 到 Camel。需要特别注意的是,由于 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。

异步 Ping Pong 示例​

前面所有示例都是同步的。流程实例会等待 Camel 路由执行完毕并返回。在某些情况下,我们可能需要 Flowable 流程实例继续执行。为此,Camel 服务任务的异步能力很有用。把 Camel 服务任务的 async 属性设为 true 即可启用该特性。

<serviceTask id="serviceAsyncPing" flowable:type="camel" flowable:async="true"/>

启用此特性后,指定的 Camel 路由由 Flowable job 执行器异步激活。当你在 Camel 路由中定义了一个队列时,Flowable 流程实例会继续执行流程定义中 Camel 服务任务之后定义的活动。Camel 路由将与流程执行完全异步地执行。如果你想在流程定义中某处等待 Camel 服务任务的响应,可以使用接收任务。

<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 引擎。

也可以把流程实例的发起人设置为某个认证用户 ID,该 ID 由 Camel 头提供。为此,首先必须在流程定义中指定一个发起人变量:

<startEvent id="start" flowable:initiator="initiator" />

然后,假设用户 ID 包含在一个名为 CamelProcessInitiatorHeader 的 Camel 头中,Camel 路由可定义如下:

from("direct:startWithInitiatorHeader")
.setHeader("CamelProcessInitiatorHeader", constant("kermit"))
.to("flowable:InitiatorCamelCallProcess?processInitiatorHeaderName=CamelProcessInitiatorHeader");

手工任务​

描述​

手工任务 定义了一个位于 BPM 引擎之外的任务。它用于对由某人完成、引擎无需知晓、也没有系统或用户界面的工作建模。对引擎而言,手工任务被当作一种直通活动处理,流程执行一旦到达便自动继续流程。

图形表示​

手工任务被可视化为一个圆角矩形,左上角带一个小“手”图标。

bpmn.manual.task

XML 表示​

<manualTask id="myManualTask" name="Call client for more information" />

Java 接收任务​

描述​

接收任务是一个简单任务,它等待某个消息到达。目前,我们只为该任务实现了 Java 语义。当流程执行到达接收任务时,流程状态会被提交到持久化存储。这意味着流程将保持在此等待状态,直到引擎收到特定消息,从而触发流程穿过接收任务继续执行。

图形表示​

接收任务被可视化为一个任务(圆角矩形),左上角带一个消息图标。消息图标为白色(黑色消息图标则表示发送语义)。

bpmn.receive.task

XML 表示​

<receiveTask id="waitState" name="wait" />

要继续一个当前正等待在此接收任务处的流程实例,必须使用到达该接收任务的执行 ID 调用 runtimeService.trigger(executionId)。下面的代码片段展示了实际用法:

ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
Execution execution = runtimeService.createExecutionQuery()
.processInstanceId(pi.getId())
.activityId("waitState")
.singleResult();
assertNotNull(execution);

runtimeService.trigger(execution.getId());

Shell 任务​

描述​

Shell 任务允许你运行 shell 脚本和命令。请注意,Shell 任务并不是 BPMN 2.0 规范中的“官方”任务(因此也没有专门的图标)。

定义 Shell 任务​

Shell 任务实现为一种专用服务任务,通过把服务任务的 type 设置为 'shell' 来定义。

<serviceTask id="shellEcho" flowable:type="shell">

Shell 任务通过字段注入进行配置。这些属性的值都可以包含 EL 表达式,在流程执行期间于运行时解析。可以设置以下属性:

属性是否必需?类型描述默认
command是String要执行的 Shell 命令。
arg1-5否String参数 1 到参数 5
wait否true/false必要时等待,直到 shell 进程终止。true
redirectError否true/false把标准错误合并到标准输出。false
cleanEnv否true/falseshell 进程不继承当前环境。false
outputVariable否String用于存放输出的变量名不记录输出。
errorCodeVariable否String用于存放任何结果错误码的变量名不记录错误级别。
directory否Stringshell 进程的默认目录当前目录

用法示例​

下面的 XML 片段展示了使用 Shell 任务的一个示例。它运行 shell 脚本 "cmd /c echo EchoTest",等待其终止,并把结果放入 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>

外部 Worker 任务​

描述​

外部 Worker 任务允许你创建应由外部 Worker 获取并执行的 Job。外部 Worker 可以通过 Java API 或 REST API 获取 Job。这与异步服务任务类似。区别在于:不是由 Flowable 执行逻辑,而是由一个可以用任意语言实现的外部 Worker 向 Flowable 查询 Job、执行它们并把结果发回 Flowable。请注意,外部 Worker 任务并不是 BPMN 2.0 规范中的“官方”任务(因此也没有专门的图标)。

定义外部 Worker 任务​

外部 Worker 任务实现为一种专用服务任务,通过把服务任务的 type 设置为 'external-worker' 来定义。

<serviceTask id="externalWorkerOrder" flowable:type="external-worker">

外部 Worker 任务通过设置 topic(可以是一个 EL 表达式)来配置,外部 Worker 用它查询要执行的 Job。

用法示例​

下面的 XML 片段展示了使用外部 Worker 任务的一个示例。外部 Worker 是一个等待状态。当执行到达该任务时,会创建一个外部 Worker Job,可由某个外部 Worker 获取。一旦外部 Worker 完成该 Job 并通知 Flowable 完成,流程的执行就会继续。

<serviceTask id="externalWorkerOrder" flowable:type="external-worker" flowable:topic="orderService" />

获取外部 Worker Job​

外部 Worker Job 通过 ManagementService#createExternalWorkerJobAcquireBuilder 获取,使用一个 ExternalWorkerJobAcquireBuilder。

List<AcquiredExternalWorkerJob> acquiredJobs = managementService.createExternalWorkerJobAcquireBuilder()
.topic("orderService", Duration.ofMinutes(30))
.acquireAndLock(5, "orderWorker-1");

使用上面这段 Java 代码即可获取外部 Worker Job。这段代码做了以下事情:

  • 查询 topic 为 orderService 的外部 Worker Job。
  • 获取并锁定这些 Job 30 分钟,等待外部 Worker 完成信号。
  • 最多获取 5 个 Job
  • Job 的持有者是 id 为 orderWorker-1 的 worker

AcquiredExternalWorkerJob 还可以访问流程变量。当外部 Worker 任务是排他的时,获取该 Job 会锁定流程实例。

完成外部 Worker Job​

外部 Worker Job 通过 ManagementService#createExternalWorkerCompletionBuilder(String, String) 完成,使用一个 ExternalWorkerCompletionBuilder。

managementService.createExternalWorkerCompletionBuilder(acquiredJob.getId(), "orderWorker-1")
.variable("orderStatus", "COMPLETED")
.complete();

一个 Job 只能由获取它的 worker 完成。否则会抛出 FlowableIllegalArgumentException。

使用上面的代码片段即可完成该任务,流程执行将继续。执行的继续是在一个新事务中异步完成的。这意味着完成一个外部 Worker 任务只会创建一个异步(新)Job 来执行完成动作(当前线程做完这件事后即返回)。模型中外部 Worker 任务之后的任何步骤都将在该事务中执行,与普通异步服务任务类似。

外部 Worker Job 的错误处理​

外部 Worker Job 有两种错误处理方式:

  • 通过 ManagementService#createExternalWorkerCompletionBuilder(String, String) 触发业务错误,使用一个 ExternalWorkerCompletionBuilder。
managementService.createExternalWorkerCompletionBuilder(acquiredJob.getId(), "orderWorker-1")
.variable("orderStatus", "FAILED")
.bpmnError("orderFailed");

使用此代码片段会在流程上设置 orderStatus 变量,并抛出一个错误码为 orderFailed 的 BPMN 错误。BPMN 错误只能由获取该 Job 的 worker 抛出。

  • 通过 ManagementService#createExternalWorkerJobFailureBuilder(String, String) 报告技术性错误,使用一个 ExternalWorkerJobFailureBuilder。
managementService.createExternalWorkerJobFailureBuilder(acquiredJob.getId(), "orderWorker-1")
.errorMessage("Failed to run job. Database not accessible")
.errorDetails("Some complex and long error details")
.retries(4)
.retryTimeout(Duration.ofHours(1))
.fail();

使用此代码片段会完成以下事情:

  • 在 Job 上设置错误消息和错误详情
  • 把该 Job 的重试次数设为 4
  • 1 小时后该 Job 才可被获取

该 Job 只能由获取它的 worker 标记为失败。如果未设置重试次数,Flowable 会自动把该 Job 的重试次数减 1。当重试次数为 0 时,该 Job 会被移到死信表(DeadLetter table),不再可被获取。

查询外部 Worker Job​

外部 Worker Job 通过 ExternalWorkerJobQuery 查询,该查询由 ManagementService#createExternalWorkerJobQuery 创建。

执行监听器​

执行监听器允许你在流程执行过程中发生某些事件时,执行外部 Java 代码或计算一个表达式。可捕获的事件有:

  • 流程实例的开始与结束。
  • 取走一条转移(顺序流)。
  • 活动的开始与结束。
  • 网关的开始与结束。
  • 中间事件的开始与结束。
  • 开始事件的结束与结束事件的开始。

下面的流程定义包含 3 个执行监听器:

<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 接口。当事件发生时(本例中是 end 事件),会调用 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 接口的委托类。这些委托类随后可在其他结构中复用,例如作为 serviceTask 的委托。

第二个执行监听器在取走该转移时被调用。请注意,该监听器元素没有定义 event,因为转移上只会触发 take 事件。在转移上定义监听器时,event 属性中的值会被忽略。

最后一个执行监听器在活动 secondTask 结束时被调用。监听器声明上没有使用 class,而是定义了一个表达式,在事件触发时对其求值/调用。

<flowable:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />

与其他表达式一样,执行变量会被解析并可使用。由于执行实现对象有一个暴露事件名的属性,可以用 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>

脚本执行监听器后来成为了一等公民,可以使用更紧凑的语法,从而避免硬编码类名:

<flowable:executionListener event="start" type="script">
<flowable:script language="groovy">
<![CDATA[
def bar = "BAR"; // local variable
execution.setVariable("var1", "test"); // test access to execution instance
]]>
</flowable:script>
</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 把两个注入字段(一个固定、另一个动态)拼接起来,并存入流程变量 '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);
}

请注意,与服务任务相同的线程安全规则也适用于此。更多信息请阅读相关章节。

在执行监听器中抛出 BPMN 错误​

可以从执行监听器代码中抛出 BPMN 错误,由模型中匹配的错误边界事件或错误开始事件捕获。这可以使用 Flowable 的 org.flowable.engine.delegate.BpmnError 类完成。示例及进一步说明请参阅抛出 BPMN 错误一节。

需要特别注意的是,在执行监听器中抛出的 BPMN 错误只会被父作用域上的错误边界事件捕获,而不会被活动的边界事件捕获。

下图说明了这一点:

bpmn.executionlistener.bpmnerror

假设 A Task 上定义了一个执行监听器,它在某个时刻抛出一个 BpmnError:只有当 Subprocess 上的错误边界事件匹配时(即图中红色那个),错误才会被捕获。此时 A Task 上的错误边界事件会被忽略。这本来也是推荐做法。

原因在于:调用执行监听器时,活动要么尚未完全初始化(对 start 监听器而言),要么作用域已被销毁(对 end 监听器而言)。

任务监听器​

任务监听器 用于在某个任务相关事件发生时执行自定义 Java 逻辑或一个表达式。

任务监听器只能在流程定义中作为用户任务的子元素添加。请注意,它还必须作为 BPMN 2.0 extensionElements 的子元素,并位于 flowable 命名空间下,因为任务监听器是 Flowable 特有的结构。

<userTask id="myTask" name="My Task" >
<extensionElements>
<flowable:taskListener event="create" class="org.flowable.MyTaskCreateListener" />
</extensionElements>
</userTask>

任务监听器 支持以下属性:

  • event(必需):触发任务监听器的任务事件类型。可能的事件有
-   **create**: occurs when the task has been created and **all task properties are set**.

- **assignment**: occurs when the task is assigned to somebody. Note: when process execution arrives in a userTask, first an *assignment* event will be fired, **before** the *create* event is fired. This might seem an unnatural order, but the reason is pragmatic: when receiving the *create* event, we usually want to inspect all properties of the task including the assignee.

- **complete**: occurs when the task is completed and just before the task is deleted from the runtime data.

- **delete**: occurs just before the task is going to be deleted. Notice that it will also be executed when task is normally finished via completeTask.
  • class:必须调用的委托类。该类必须实现 org.flowable.task.service.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>
  • 脚本任务监听器后来成为了一等公民,可以使用更紧凑的语法,从而避免硬编码类名:
<flowable:taskListener event="complete" type="script">
<flowable:script language="groovy" resultVariable="myVar">
<![CDATA[
def bar = "BAR"; // local variable
task.setOwner("kermit"); // test access to task instance
bar // implicit return value
]]>
</flowable:script>
</flowable:taskListener>

在任务监听器中抛出 BPMN 错误​

请跳转至在执行监听器中抛出 BPMN 错误一节,因为任务监听器适用相同规则。

多实例(for each)​

描述​

多实例活动 是为业务流程中某个步骤定义重复执行的一种方式。用编程概念来说,多实例等价于 for each 结构:它允许你针对给定集合中的每一项,顺序地或并行地执行某个步骤,甚至整个子流程。

多实例 是一种普通活动,只是定义了额外属性(称为 '多实例特性'),使该活动在运行时被执行多次。以下活动可以成为 _多实例活动:*

网关或事件不能成为多实例。

按照 BPMN 2.0 规范的要求,为每个实例创建的执行的每个父执行都将拥有以下变量:

  • nrOfInstances:实例总数
  • nrOfActiveInstances:当前活跃(尚未完成)的实例数。对于顺序多实例,该值始终为 1。
  • nrOfCompletedInstances:已完成的实例数。

这些值可通过调用 execution.getVariable(x) 方法获取。

此外,每个被创建的执行都会有一个执行局部变量(对其他执行不可见,也不存放在流程实例级别):

  • loopCounter:表示该特定实例在 for-each 循环中的索引。loopCounter 变量可以通过 Flowable 的 elementIndexVariable 属性重命名。

图形表示​

如果一个活动是多实例的,则在活动底部用三条短横线表示。三条竖直线表示各实例将并行执行,三条水平线表示顺序执行。

bpmn.multi.instance

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 schema 的限制,它们不能包含表达式。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 属性会被作为表达式解析。当表达式解析为 String 而非集合时(无论是因为它是静态字符串值,还是表达式本身解析为 String),所得值将被用作变量名,去获取集合流程变量。

例如,在下面的片段中,假定集合存放在 assigneeList 流程变量中:

<userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="true"
flowable:collection="assigneeList" flowable:elementVariable="assignee" >
</multiInstanceLoopCharacteristics>
</userTask>

例如,假设 myService.getCollectionVariableName() 返回一个 String 值,该值将被用作变量名,去获取作为流程变量存放的集合。

<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.multi.instance.boundary.event

在这里,当定时器触发时,该子流程的所有实例都会被销毁,无论有多少实例、或哪些内部活动尚未完成。

多实例与执行监听器​

把执行监听器与多实例结合使用时有一个注意事项。以下面这段 BPMN 2.0 XML 片段为例,它定义在与设置 multiInstanceLoopCharacteristics XML 元素相同的层级:

<extensionElements>
<flowable:executionListener event="start" class="org.flowable.MyStartListener"/>
<flowable:executionListener event="end" class="org.flowable.MyEndListener"/>
</extensionElements>

对于普通 BPMN 活动,活动开始和结束时会调用这些监听器。

但是,当活动是多实例时,行为有所不同:

  • 进入多实例活动时、在执行任何内部活动之前,会抛出一个 start 事件。此时 loopCounter 变量尚未设置(为 null)。
  • 对于每个实际被访问的活动,都会抛出一个 start 事件。此时 loopCounter 变量已设置。

end 事件适用相同逻辑:

  • 离开实际活动后,会抛出一个 end 事件。此时 loopCounter 变量已设置。
  • 当多实例活动作为一个整体结束时,会抛出一个 end 事件。此时 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 事件。start 执行监听器被调用。此时 loopCounter 和 assignee 变量不会设置(为 null)。
  • 为每个活动实例抛出一个 start 事件。start 执行监听器被调用三次。此时 loopCounter 和 assignee 变量已设置(不为 null)。
  • 因此,start 执行监听器总共被调用四次。

请注意,当 multiInstanceLoopCharacteristics 定义在非子流程的其他元素上时,同样适用。例如,如果上面的示例是一个简单的 userTask,同样的推理依然成立。

补偿处理器​

描述​

如果某个活动用于补偿另一个活动的影响,可以把它声明为一个补偿处理器。补偿处理器在正常流程中并不存在,只在抛出补偿事件时才执行。

补偿处理器不得有入向或出向顺序流。

补偿处理器必须通过一条有向关联与一个补偿边界事件关联。

图形表示​

如果一个活动是补偿处理器,会在其中央底部区域显示补偿事件图标。下面这张流程图片段展示了一个服务任务,它附着了一个补偿边界事件,而该边界事件又与一个补偿处理器关联。注意“cancel hotel reservation”服务任务底部中央区域的补偿处理器图标。

bpmn.boundary.compensation.event

XML 表示​

要把一个活动声明为补偿处理器,需要把属性 isForCompensation 设为 true:

<serviceTask id="undoBookHotel" isForCompensation="true" flowable:class="...">
</serviceTask>

子流程与调用活动​

子流程​

描述​

子流程 是一种包含其他活动、网关、事件等的活动,它本身构成一个流程,是更大流程的一部分。子流程 完全定义在父流程内部(因此常被称为嵌入式子流程)。

子流程有两个主要用例:

  • 子流程支持分层建模。许多建模工具允许把多流程折叠起来,隐藏子流程的所有细节,从而得到业务流程的高层端到端概览。
  • 子流程为事件创建一个新的作用域。在子流程执行期间抛出的事件,可以被子流程边界上的边界事件捕获,从而为该事件创建一个仅限于子流程内的作用域。

使用子流程确实会带来一些约束:

  • 一个子流程只能有一个“空开始事件”,不允许其他开始事件类型。子流程必须至少有一个“结束事件”。请注意,BPMN 2.0 规范允许省略子流程中的开始和结束事件,但当前 Flowable 实现不支持这一点。
  • 顺序流不能跨子流程边界。

图形表示​

子流程被可视化为一个典型活动(圆角矩形)。如果子流程是折叠的,则只显示名称和一个加号,给出流程的高层概览:

bpmn.collapsed.subprocess

如果子流程是展开的,则子流程的各个步骤显示在子流程边界之内:

bpmn.expanded.subprocess

使用子流程的一个主要原因是为某个事件定义作用域。下面这个流程模型说明了这一点:investigate software/investigate hardware 两个任务需要并行完成,但两个任务都必须在某个时间内完成,然后才咨询 Level 2 support。这里,定时器的作用域(即活动必须在其中按时完成的范围)由子流程限定。

bpmn.subprocess.with.boundary.timer

XML 表示​

子流程通过 subProcess 元素定义。属于该子流程的所有活动、网关、事件等都需要包裹在该元素内。

<subProcess id="subProcess">

<startEvent id="subProcessStart" />

... other Sub-Process elements ...

<endEvent id="subProcessEnd" />

</subProcess>

事件子流程​

描述​

事件子流程是 BPMN 2.0 中新增的。事件子流程是一种由事件触发的子流程。事件子流程可以加在流程级别,也可以加在任意子流程级别。用于触发事件子流程的事件通过一个开始事件配置。由此可知,事件子流程不支持“空开始事件”。事件子流程可以用消息事件、错误事件、信号事件、定时器事件或补偿事件等来触发。当承载该事件子流程的作用域(流程实例或子流程)被创建时,会创建对该开始事件的订阅;当作用域被销毁时,订阅被移除。

事件子流程可以是中断型或非中断型。中断型子流程会取消当前作用域内的所有执行。非中断型事件子流程会派生一条新的并发执行。对于承载它的作用域的每次激活,中断型事件子流程只能被触发一次,而非中断型事件子流程可以被多次触发。子流程是否中断,由触发该事件子流程的开始事件配置。

事件子流程不得有任何入向或出向顺序流。由于事件子流程由事件触发,入向顺序流没有意义。当事件子流程结束时,要么结束当前作用域(如果是中断型事件子流程),要么结束为非中断型子流程派生的并发执行。

当前限制:

  • Flowable 支持用错误、定时器、信号和消息开始事件触发的事件子流程。

图形表示​

事件子流程可被可视化为一个带虚线轮廓的嵌入式子流程。

bpmn.subprocess.eventSubprocess

XML 表示​

事件子流程在 XML 中的表示方式与嵌入式子流程相同。此外,属性 triggeredByEvent 必须取值为 true:

<subProcess id="eventSubProcess" triggeredByEvent="true">
...
</subProcess>

示例​

下面是一个用错误开始事件触发的事件子流程示例。该事件子流程位于“流程级别”,换句话说,其作用域为整个流程实例:

bpmn.subprocess.eventSubprocess.example.1

该事件子流程在 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>

如前所述,事件子流程也可以加在一个嵌入式子流程上。如果加在嵌入式子流程上,它就成了边界事件的一种替代方案。请看下面两张流程图。两种情况下,嵌入式子流程都抛出一个错误事件。两种情况下错误都被捕获,并通过一个用户任务处理。

bpmn.subprocess.eventSubprocess.example.2a

与之相对的是:

bpmn.subprocess.eventSubprocess.example.2b

两种情况下执行的是相同的任务。但是,这两种建模方案之间存在差异:

  • 嵌入式子流程使用执行其所在作用域的同一条执行来执行。这意味着嵌入式子流程可以访问其作用域局部的变量。使用边界事件时,为执行嵌入式子流程而创建的执行会被离开边界事件的顺序流删除。这意味着嵌入式子流程创建的变量将不再可用。
  • 使用事件子流程时,事件完全由它所加入的子流程处理。使用边界事件时,事件由父流程处理。

这两点差异可以帮助你判断:对于解决某个特定的流程建模或实现问题,边界事件还是嵌入式子流程更合适。

事务子流程​

描述​

事务子流程是一种嵌入式子流程,可用于把多个活动归组到一个事务中。事务是一个逻辑工作单元,它允许把一组独立活动归组在一起,使它们要么集体成功,要么集体失败。

事务的可能结果: 一个事务可以有三种不同结果:

  • 如果事务既未被取消也未因风险(hazard)终止,则它是成功的。如果事务子流程成功,则通过出向顺序流离开它。如果之后流程中抛出补偿事件,成功的事务可能会被补偿。注意: 与“普通”嵌入式子流程一样,事务成功完成后,可以通过一个中间抛出补偿事件对其进行补偿。
  • 如果某条执行到达取消结束事件,则事务被取消。此时,所有执行都被终止并移除。然后设置一条剩余执行到取消边界事件,触发补偿。补偿完成后,通过取消边界事件的出向顺序流离开事务子流程。
  • 如果抛出了一个在事务子流程作用域内未被捕获的错误事件,则事务因*风险(hazard)*而结束。即使错误在事务子流程边界上被捕获,同样适用。在这些情况下,不执行补偿。

下图说明了这三种不同结果:

bpmn.transaction.subprocess.example.1

与 ACID 事务的关系: 重要的是不要把 BPMN 事务子流程与技术性(ACID)事务混淆。BPMN 事务子流程并不是一种圈定技术性事务作用域的方式。要理解 Flowable 中的事务管理,请阅读并发性与事务一节。BPMN 事务在以下方面不同于技术性事务:

  • ACID 事务通常是短生命周期的,而 BPMN 事务可能需要数小时、数天甚至数月才能完成。设想这样一种情况:事务所归组的活动中有一个是用户任务——通常人的响应时间比应用长。或者,在另一种情形下,BPMN 事务可能要等待某个业务事件发生,例如某个特定订单已被履约。这类操作的完成时间通常远长于更新数据库中的一条记录,或用事务队列存储一条消息。
  • 由于不可能把技术性事务的作用域限定为一个业务活动的持续时间,BPMN 事务通常跨越多个 ACID 事务。
  • 由于 BPMN 事务跨越多个 ACID 事务,我们丧失了 ACID 特性。例如,看上面给出的示例。假设“book hotel”和“charge credit card”操作在不同的 ACID 事务中执行。再假设“book hotel”活动成功。现在我们就有了一个中间不一致状态:我们已经预订了酒店,但还没有信用卡扣款。在 ACID 事务中,我们同样会顺序执行不同操作,因而也会有一个中间不一致状态。这里的不同之处在于,这个不一致状态在事务作用域之外是可见的。例如,如果预订是通过外部预订服务完成的,使用同一预订服务的其他方可能已经看到酒店已被预订。这意味着在实现业务事务时,我们完全丧失了隔离性(诚然,在处理 ACID 事务时我们通常也会放松隔离性以获得更高并发,但在那里我们有细粒度控制,中间不一致只持续极短时间)。
  • BPMN 业务事务不能按传统意义回滚。由于它跨越多个 ACID 事务,当 BPMN 事务被取消时,其中某些 ACID 事务可能已经提交。此时它们无法再回滚。

由于 BPMN 事务本质上是长生命周期的,缺乏隔离性和回滚机制需要以不同方式应对。实践中,通常没有比用领域特定方式解决这些问题更好的办法:

  • 回滚通过补偿来执行。如果在事务作用域内抛出取消事件,则所有成功执行且带有补偿处理器的活动的影响都会被补偿。
  • 缺乏隔离性也常常用领域特定方案来应对。例如,在上面的示例中,在我们真正确认第一位客户能付款之前,酒店房间可能已经显示为被第二位客户预订。从业务角度看这可能不可取,预订服务可能会选择允许一定数量的超订。
  • 此外,由于事务在发生风险时可能被中止,预订服务还必须处理这样的情形:酒店房间已被预订,但从未尝试付款(因为事务被中止了)。在这种情况下,预订服务可能选择一种策略:把酒店房间保留最长一段时间,如果届时仍未收到付款,则取消预订。

总而言之:ACID 事务为这类问题提供了通用解决方案(回滚、隔离级别和启发式结果),而在实现业务事务时,我们需要为这些问题寻找领域特定的解决方案。

当前限制:

  • BPMN 规范要求流程引擎对底层事务协议发出的事件作出反应,例如,如果底层协议中发生取消事件,则取消该事务。作为一个可嵌入引擎,Flowable 当前不支持这一点。由此带来的一些影响,请见下文关于一致性的段落。

基于 ACID 事务与乐观并发的一致性: BPMN 事务保证一致性,意思是:要么所有活动都成功完成,要么如果某个活动无法执行,则所有其他成功活动的影响都会被补偿。因此无论哪种情况,我们最终都处于一致状态。但是,重要的是要认识到,在 Flowable 中,BPMN 事务的一致性模型是叠加在流程执行的一致性模型之上的。Flowable 以事务方式执行流程,并使用乐观锁来处理并发。在 Flowable 中,BPMN 错误、取消和补偿事件都构建在同样的 ACID 事务和乐观锁之上。例如,取消结束事件只有在真正被到达时才能触发补偿;如果此前某个服务任务抛出了某个未声明的异常,它就不会被到达。或者,如果底层 ACID 事务中的其他参与者把事务设为 rollback-only 状态,补偿处理器的影响就无法提交。或者,当两条并发执行同时到达取消结束事件时,补偿可能被触发两次并因乐观锁异常而失败。这一切都是为了说明:在 Flowable 中实现 BPMN 事务时,适用与实现“普通”流程和子流程相同的那套规则。因此,要有效保证一致性,重要的是以一种确实把乐观、事务式执行模型考虑在内的方式来实现流程。

图形表示​

事务子流程被可视化为一个带双轮廓的嵌入式子流程。

bpmn.transaction.subprocess

XML 表示​

事务子流程在 XML 中使用 transaction 标签表示:

<transaction id="myTransaction" >
...
</transaction>

示例​

下面是一个事务子流程示例:

bpmn.transaction.subprocess.example.2

调用活动(子流程)​

描述​

BPMN 2.0 区分了两种看起来非常相似的结构:一种是普通的 子流程(常被称为嵌入式子流程),另一种是调用活动。从概念上讲,当流程执行到达该活动时,两者都会调用一个子流程。

区别在于:调用活动引用的是流程定义外部的一个流程,而 子流程 嵌入在原始流程定义内部。调用活动的主要用例是提供一个可复用的流程定义,可以被多个其他流程定义调用。

当流程执行到达调用活动时,会创建一条新的执行,它是到达该调用活动那条执行的子执行。然后用这条子执行来执行子流程,与在普通流程中一样,可能创建并行的子执行。父执行会一直等待,直到子流程完全结束,然后再继续原来的流程。

图形表示​

调用活动的可视化方式与子流程相同,但带有粗边框(折叠和展开)。取决于建模工具,调用活动也可以展开,但默认可视化为折叠的子流程。

bpmn.collapsed.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 标准元素 dataInputAssociation 和 dataOutputAssociation 提供了一个 Flowable 扩展作为快捷写法;后者只有在你以 BPMN 2.0 标准方式声明流程变量时才生效。

这里也可以使用表达式:

<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。

callActivity 元素还支持通过一个 Flowable 自定义属性扩展来设置子流程实例上的业务键。'businessKey' 属性可用于在子流程实例上设置自定义业务键值。

<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:businessKey="${myVariable}">
...
</callActivity>

把 'inheritBusinessKey' 属性设为 true,会把调用流程中定义的业务键值设置到子流程上。

<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:inheritBusinessKey="true">
...
</callActivity>

引用同一部署中的流程​

默认行为是取最新部署的流程定义来启动被引用流程。有些用例下,你希望被引用流程与主流程在同一个部署中部署,以便在部署时使用被引用流程的同一状态。这要求你把主流程和被引用流程部署在同一个部署单元内,使这些流程定义对该部署有相同的引用。

要使用同一部署中的被引用流程,可以在 callActivity 元素上把 sameDeployment 属性设为 true,如下例所示:

<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:sameDeployment="true">
...
</callActivity>

默认情况下,sameDeployment 属性设为 false。

当在当前租户中按 key 找不到流程定义时,callActivity 元素提供了回退选项。当属性 fallbackToDefaultTenant 设为 true 时,callActivity 会在无租户的流程定义中按 key 查找流程定义。

<callActivity id="callSubProcess" calledElement="checkCreditProcess" flowable:fallbackToDefaultTenant="true" >
...
</callActivity>

默认值为 false。

按 id 引用流程定义​

默认行为是按 key 取最新部署的流程定义来启动被引用流程。有些用例下,你希望按 id 精确引用某个流程定义。

要按 id 使用被引用流程定义,使用 flowable:calledElementType 并取值 id。允许值为 key 和 id。

<callActivity id="callSubProcess" calledElement="UNIQUE-PROCESS_DEFINITION-ID" flowable:calledElementType="id">
...
</callActivity>

默认情况下,flowable:calledElementType 属性设为 key。

示例​

下面这张流程图展示了一个简单的订单处理。由于检查客户信用可能是许多其他流程的通用步骤,这里把检查信用步骤建模为一个调用活动。

bpmn.call.activity.super.process

该流程如下:

<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" />

子流程如下:

bpmn.call.activity.sub.process

子流程的流程定义没有什么特别之处。它完全可以不被其他流程调用而单独使用。

异步完成​

在某些特定用例中,需要让调用活动的完成是异步(且排他)的。

请看下面这个流程定义。假设被调用的流程定义只由标记为异步且排他的服务任务组成。这些子流程实例的所有步骤都将异步执行。当最后一个异步服务任务执行时,会访问结束事件。此时控制权会交回父流程实例。

bpmn.call.activity.async.complete

问题在于,异步加锁发生在流程实例级别。在这种情况下,当访问最后一个服务任务和结束事件时,子流程实例会被锁定。这意味着当控制权交回父流程实例、即调用活动完成时,父流程实例并未被锁定。当使用多实例时,这意味着有 x 个实例尝试完成该调用活动。即使所有步骤都使用排他,这仍意味着每个子流程实例都会并发地尝试更新父流程实例,由于'错误'的流程实例被用于排他锁,可能导致 FlowableOptimisticLockingException。

这个问题的解决方案是为调用活动标记 async complete 标志。当为 true 时,会使用父流程实例创建一个新的异步作业。当创建排他锁时,该锁会持有在父流程实例上,调用活动的多实例完成将针对每条子执行分别、排他地处理。

事务与并发性​

异步延续​

Flowable 以事务方式执行流程,可按需配置。先看 Flowable 通常如何圈定事务。当你触发 Flowable(启动一个流程、完成一个任务、给一条执行发信号)时,Flowable 会在流程中向前推进,直到每条活跃执行路径都到达等待状态。更具体地说,它对流程图做一次深度优先搜索,当每个执行分支都到达等待状态时返回。等待状态是一个“稍后”才执行的任务,意味着 Flowable 会把当前执行持久化,然后等待再次被触发。触发既可来自外部(例如用户任务或接收消息任务),也可来自 Flowable 自身(例如定时器事件)。下图说明了这一点:

async.example.no.async

我们看到一段 BPMN 流程,含一个用户任务、一个服务任务和一个定时器事件。完成用户任务并校验地址属于同一个工作单元,因此应该原子地成功或失败。这意味着如果服务任务抛出异常,我们希望回滚当前事务,使执行回退到用户任务,用户任务仍保留在数据库中。这也是 Flowable 的默认行为。在 (1) 处,一个应用或客户端线程完成该任务。在同一个线程中,Flowable 现在执行该服务并向前推进,直到到达等待状态——本例中是定时器事件 (2)。然后它把控制权返回给调用方 (3),并可能提交事务(如果该事务是由 Flowable 启动的)。

在某些情况下这并不是我们想要的。有时我们需要对流程中的事务边界进行自定义控制,以便圈定逻辑工作单元。这时异步延续就派上用场了。请看下面这个流程(片段):

async.example.async

这一次我们完成用户任务、生成一张发票,然后把发票发给客户。这一次生成发票不属于同一个工作单元,因此如果生成发票失败,我们不希望回滚用户任务的完成。我们希望 Flowable 做的是:完成用户任务 (1)、提交事务、把控制权返回给调用应用。然后我们希望在一个后台线程中异步生成发票。这个后台线程就是 Flowable 作业执行器(实际是一个线程池),它周期性地轮询数据库中的作业。在幕后,当我们到达“generate invoice”任务时,会为 Flowable 创建一个作业“消息”以便稍后继续流程,并把它持久化到数据库。然后这个作业被作业执行器取走并执行。我们还给本地作业执行器一个小提示,告知有新作业,以提升性能。

要使用此特性,可以使用 flowable:async="true" 扩展。例如,服务任务会像这样:

<serviceTask id="service1" name="Generate Invoice"
flowable:class="my.custom.Delegate"
flowable:async="true" />

flowable:async 可用于以下 BPMN 任务类型:task、serviceTask、scriptTask、businessRuleTask、sendTask、receiveTask、userTask、subProcess、callActivity。

在 userTask、receiveTask 或其他等待状态上,异步延续允许我们在单独的线程/事务中执行 start 执行监听器。

失败重试​

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 分钟。

排他作业​

在最近的版本中,JobExecutor 保证来自同一个流程实例的作业绝不会被并发执行。为什么要这样?

为什么需要排他作业?​

请看下面这个流程定义:

bpmn.why.exclusive.jobs

我们有一个并行网关,后面跟着三个服务任务,它们都执行异步延续。结果是三个作业被加入数据库。一旦这样的作业存在于数据库中,就可以被 JobExecutor 处理。JobExecutor 获取这些作业,并把它们交给一个工作线程池,由后者真正处理作业。这意味着,通过使用异步延续,你可以把工作分发到这个线程池(在集群场景下甚至跨集群中的多个线程池)。这通常是件好事。但是它也有一个固有问题:一致性。请看服务任务之后的并行汇聚。当某个服务任务执行完成时,我们到达并行汇聚,需要决定是等待其他执行,还是可以继续向前。这意味着对于每个到达并行汇聚的分支,我们都要做一个判断:是可以继续,还是需要等待其他分支上的一条或多条执行。

为什么这是个问题?由于服务任务都配置了异步延续,对应的作业有可能被同时获取,并被 JobExecutor 分派到不同的工作线程。后果是:执行服务的事务、以及三条独立执行到达并行汇聚的事务可能发生重叠。如果重叠,每个事务都不会“看到”另一个事务正并发到达同一个并行汇聚,因而都假定自己必须等待其他执行。但是,如果每个事务都假定必须等待其他事务,那么在并行汇聚之后没有任何一个会继续流程,流程实例将永远停留在该状态。

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 不会并发执行同一流程实例的作业,但仍会并发执行多个实例。从整体吞吐角度看,这在大多数场景下是可取的,因为它通常让单个实例完成得更快。此外,执行同一流程实例后续作业所需的数据,已经在执行该集群节点的缓存中。如果作业没有这种节点亲和性,这些数据可能需要再次从数据库获取。

流程启动授权​

默认情况下,任何人都可以为已部署的流程定义启动新的流程实例。流程启动授权功能允许你定义用户和组,以便 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'。不带任何指示符时默认为组类型。也可以使用 <process> 标签的属性,即 <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();

数据对象​

BPMN 允许把数据对象定义为流程或子流程元素的一部分。按照 BPMN 规范,可以包含从 XSD 定义导入的复杂 XML 结构。作为 Flowable 支持数据对象的第一步,目前支持以下 XSD 类型:

<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>
...