BPMN 2.0 结构
本章介绍 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 所支持。其想法是:在执行“做某事”任务时抛出的错误,被边界错误事件捕获,通过信号抛出事件传播到并行的执行路径,然后中断“并行做某事”任务。到目前为止,Flowable 会按预期执行。信号会传播到捕获用的边界事件并中断该任务。**然而,由于信号的广播语义,它也会被传播给所有其他已订阅该信号事件的流程实例。**在这种情况下,这可能不是我们想要的。
注意: 信号事件不会与某个特定流程实例进行任何形式的关联(correlation)。相反,它会被广播给所有流程实例。如果你只需要将信号投递给某个特定流程实例,请手动进行关联,并使用 signalEventReceived(String signalName, String executionId),配合适当的查询机制