跳到主要内容

CMMN 1.1

Doubao 中英对照 CMMN 1.1

什么是 CMMN?​

案例管理建模与表示法(CMMN)是由 对象管理组织 制定的一种标准表示法和正式规范,用于表示用例模型。

Flowable 包含:

  • 一个 CMMN 1.1 建模器,用于创建 CMMN 1.1 用例模型。
  • 一个 Java 引擎,可导入并执行 CMMN 1.1 用例模型。
  • 一个演示 UI,用于执行用例模型,让用户可以查看并完成人工任务(及其表单)。

基本概念与术语​

下图展示了一个简单的 CMMN 1.1 图:

cmmn basic concepts

用例模型总是可视化为某种 文件夹,其中包含所有用例元素。每个用例模型都包含一个计划模型,各项会被计划到其上。

计划模型的元素称为计划项。每个计划项都有一个计划项定义,给出其类型以及运行时可能的配置选项。例如,在上图中,有三个人工任务计划项和一个里程碑。计划项的其他例子有流程任务、用例任务和阶段。

将用例模型部署到 Flowable CMMN 引擎后,就可以基于该用例模型启动用例实例。用例模型中定义的计划项同样具有计划项实例这一运行时表示,它由 Flowable API 暴露并可通过其查询。计划项实例具有 CMMN 1.1 规范中定义的状态生命周期,这是引擎工作方式的核心。完整细节请查阅 CMMN 1.1 规范的 8.4.2 节。

计划项可以有哨兵:当一个哨兵"守护"计划项的激活时,称该计划项具有进入准则。这些准则指定了必须满足才能触发哨兵的条件。例如,在上图中,"Milestone One"计划项在用例实例启动后处于可用状态,但当人工任务 A 和 B 都完成时,它就被激活(用 CMMN 1.1 规范的术语说:它从可用状态转移到活动状态)。注意,哨兵在其 if 部分中可以有复杂表达式,这些表达式不被可视化,从而支持复杂得多的功能。还要注意,可以有多个哨兵,不过只需满足其中一个就能触发状态转换。

计划项和计划模型也可以带有退出准则的哨兵,用于指定触发退出该计划项的条件。在上图中,当人工任务 C 完成时,整个计划模型都会退出(此时处于活动状态的所有子元素也一并退出)。

CMMN 1.1 在作为规范一部分的 XSD 中定义了标准 XML 格式。供参考,上图中的示例用 XML 表示如下。

几点说明:

  • 上面的四个计划项都在 XML 中,它们通过 definitionRef 引用各自的定义。实际的定义位于 casePlanModel 元素的底部。
  • 计划项拥有准则(进入或退出),这些准则引用 sentry(而不是反过来)。
  • XML 还包含图如何可视化的信息(x 和 y 坐标、宽度和高度等),下面省略了这些内容。当与其他 CMMN 1.1 建模工具交换用例模型时,这些元素对于保持正确的可视化表示很重要。
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/cmmn"
xmlns:cmmndi="http://www.omg.org/spec/CMMN/20151109/CMMNDI"
xmlns:dc="http://www.omg.org/spec/CMMN/20151109/DC"
xmlns:di="http://www.omg.org/spec/CMMN/20151109/DI"
targetNamespace="http://www.flowable.org/casedef">
<case id="simpleExample" name="Simple Example">
<casePlanModel id="casePlanModel" name="My Case">
<planItem id="planItem1" name="Human task A"
definitionRef="sid-88199E7C-7655-439C-810B-8849FC52D3EB"></planItem>
<planItem id="planItem2" name="Milestone One"
definitionRef="sid-8BF8A774-A8A7-4F1A-95CF-1E0D61EE5A47">
<entryCriterion id="sid-62CC4A6D-B29B-4129-93EA-460253C45CDF"
sentryRef="sentry1"></entryCriterion>
</planItem>
<planItem id="planItem3" name="Human task B"
definitionRef="sid-A1FB8733-0DBC-4B38-9830-CBC4D0C4B802"></planItem>
<planItem id="planItem4" name="Human task C"
definitionRef="sid-D3970AFC-7391-4BA7-95BA-51C64D2F41E9"></planItem>
<sentry id="sentry1">
<planItemOnPart id="sentryOnPart1" sourceRef="planItem1">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<planItemOnPart id="sentryOnPart2" sourceRef="planItem3">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<sentry id="sentry2">
<planItemOnPart id="sentryOnPart3" sourceRef="planItem4">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<humanTask id="sid-88199E7C-7655-439C-810B-8849FC52D3EB"
name="Human task A"></humanTask>
<milestone id="sid-8BF8A774-A8A7-4F1A-95CF-1E0D61EE5A47"
name="Milestone One"></milestone>
<humanTask id="sid-A1FB8733-0DBC-4B38-9830-CBC4D0C4B802"
name="Human task B"></humanTask>
<humanTask id="sid-D3970AFC-7391-4BA7-95BA-51C64D2F41E9"
name="Human task C"></humanTask>
<exitCriterion id="sid-422626DB-9B40-49D8-955E-641AB96A5BFA"
sentryRef="sentry2"></exitCriterion>
</casePlanModel>
</case>
<cmmndi:CMMNDI>
<cmmndi:CMMNDiagram id="CMMNDiagram_simpleExample">
...
</cmmndi:CMMNDiagram>
</cmmndi:CMMNDI>
</definitions>

编程式示例​

在本节中,我们将构建一个简单的用例模型,并在一个简单的命令行示例中通过 Flowable CMMN 引擎的 Java API 以编程方式执行它。

我们要构建的用例模型是一个简化的员工入职用例,包含两个阶段:准员工入职前的阶段和入职后的阶段。在第一阶段,由 HR 部门的人完成任务;在第二阶段,则由员工自己完成。此外,在任何时候,准员工都可以拒绝这份工作并停止整个用例实例。

注意,这里只使用了阶段和人工任务。在真实的用例模型中,很可能还会有其他计划项类型,例如里程碑、嵌套阶段、自动化任务等等。

cmmn.programmatic.example

该用例模型的 XML 如下:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/cmmn"
xmlns:cmmndi="http://www.omg.org/spec/CMMN/20151109/CMMNDI"
xmlns:dc="http://www.omg.org/spec/CMMN/20151109/DC"
xmlns:di="http://www.omg.org/spec/CMMN/20151109/DI"
targetNamespace="http://www.flowable.org/casedef">
<case id="employeeOnboarding" name="Simple Example">
<casePlanModel id="casePlanModel" name="My Case">
<planItem id="planItem5" name="Prior to starting"
definitionRef="sid-025D29E8-BA9B-403D-A684-8C5B52185642"></planItem>
<planItem id="planItem8" name="After starting"
definitionRef="sid-8459EF32-4F4C-4E9B-A6E9-87FDC2299044">
<entryCriterion id="sid-50B5F12D-FE75-4D05-9148-86574EE6C073"
sentryRef="sentry2"></entryCriterion>
</planItem>
<planItem id="planItem9" name="Reject job"
definitionRef="sid-134E885A-3D58-417E-81E2-66A3E12334F9"></planItem>
<sentry id="sentry2">
<planItemOnPart id="sentryOnPart4" sourceRef="planItem5">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<sentry id="sentry3">
<planItemOnPart id="sentryOnPart5" sourceRef="planItem9">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<stage id="sid-025D29E8-BA9B-403D-A684-8C5B52185642" name="Prior to starting">
<planItem id="planItem1" name="Create email address"
definitionRef="sid-EA434DDD-E1BE-4AC1-8520-B19ACE8782D2"></planItem>
<planItem id="planItem2" name="Allocate office"
definitionRef="sid-505BA223-131A-4EF0-ABAD-485AEB0F2C96"></planItem>
<planItem id="planItem3" name="Send joining letter to candidate"
definitionRef="sid-D28DBAD5-0F5F-45F4-8553-3381199AC45F">
<entryCriterion id="sid-4D88C79D-8E31-4246-9541-A4F6A5720AC8"
sentryRef="sentry1"></entryCriterion>
</planItem>
<planItem id="planItem4" name="Agree start date"
definitionRef="sid-97A72C46-C0AD-477F-86DD-85EF643BB97D"></planItem>
<sentry id="sentry1">
<planItemOnPart id="sentryOnPart1" sourceRef="planItem1">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<planItemOnPart id="sentryOnPart2" sourceRef="planItem2">
<standardEvent>complete</standardEvent>
</planItemOnPart>
<planItemOnPart id="sentryOnPart3" sourceRef="planItem4">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<humanTask id="sid-EA434DDD-E1BE-4AC1-8520-B19ACE8782D2"
name="Create email address"
flowable:candidateGroups="hr"></humanTask>
<humanTask id="sid-505BA223-131A-4EF0-ABAD-485AEB0F2C96"
name="Allocate office"
flowable:candidateGroups="hr"></humanTask>
<humanTask id="sid-D28DBAD5-0F5F-45F4-8553-3381199AC45F"
name="Send joining letter to candidate"
flowable:candidateGroups="hr"></humanTask>
<humanTask id="sid-97A72C46-C0AD-477F-86DD-85EF643BB97D"
name="Agree start date"
flowable:candidateGroups="hr"></humanTask>
</stage>
<stage id="sid-8459EF32-4F4C-4E9B-A6E9-87FDC2299044"
name="After starting">
<planItem id="planItem6" name="New starter training"
definitionRef="sid-DF7B9582-11A6-40B4-B7E5-EC7AC6029387"></planItem>
<planItem id="planItem7" name="Fill in paperwork"
definitionRef="sid-7BF2B421-7FA0-479D-A8BD-C22EBD09F599"></planItem>
<humanTask id="sid-DF7B9582-11A6-40B4-B7E5-EC7AC6029387"
name="New starter training"
flowable:assignee="${potentialEmployee}"></humanTask>
<humanTask id="sid-7BF2B421-7FA0-479D-A8BD-C22EBD09F599"
name="Fill in paperwork"
flowable:assignee="${potentialEmployee}"></humanTask>
</stage>
<humanTask id="sid-134E885A-3D58-417E-81E2-66A3E12334F9" name="Reject job"
flowable:assignee="${potentialEmployee}"></humanTask>
<exitCriterion id="sid-18277F30-E146-4B3E-B3C9-3F1E187EC7A8"
sentryRef="sentry3"></exitCriterion>
</casePlanModel>
</case>
</definitions>

首先,创建一个新项目并添加 flowable-cmmn-engine 依赖(这里以 Maven 为例)。同时还添加了 H2 依赖,因为稍后将使用 H2 作为嵌入式数据库。

<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-cmmn-engine</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>

Flowable CMMN API 的设计与其他 Flowable API 和概念保持一致。因此,熟悉 BPMN 或 DMN API 的人很容易上手。与其他引擎一样,第一行代码就是创建一个 CmmnEngine。这里使用默认的内存配置,以 H2 作为数据库:

public class Main {
public static void main(String[] args) {
CmmnEngine cmmnEngine
= new StandaloneInMemCmmnEngineConfiguration().buildCmmnEngine();
}
}

注意,CmmnEngineConfiguration 暴露了许多配置选项,用于调整 CMMN 引擎的各项设置。

把上面的 XML 放入一个文件,例如 my-case.cmmn(或 .cmmn.xml)。对于 Maven,应将其放在 src/main/resources 文件夹中。

要让引擎知道该用例模型,首先需要将其部署。这通过 CmmnRepositoryService 完成:

CmmnRepositoryService cmmnRepositoryService = cmmnEngine.getCmmnRepositoryService();
CmmnDeployment cmmnDeployment = cmmnRepositoryService.createDeployment()
.addClasspathResource("my-case.cmmn")
.deploy();

部署该 XML 会返回一个 CmmnDeployment。一个部署可以包含许多用例模型和构件。上面这个具体的用例模型定义会作为 CaseDefinition 存储。可以通过执行一次 CaseDefinitionQuery 来验证这一点:

List<CaseDefinition> caseDefinitions = cmmnRepositoryService.createCaseDefinitionQuery().list();
System.out.println("Found " + caseDefinitions.size() + " case definitions");

引擎中有了 CaseDefinition,现在就可以为该用例模型定义启动一个 CaseInstance。既可以使用查询结果并将其传入下面的代码片段,也可以直接使用用例定义的 key(如下所示)。

注意,在启动 CaseInstance 时,我们还传入了数据:一个作为变量的 potentialEmployee 标识符。稍后该变量会在人工任务中使用,以把任务分配给正确的人(参见人工任务上的 assignee="${potentialEmployee}" 属性)。

CmmnRuntimeService cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService();
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("employeeOnboarding")
.variable("potentialEmployee", "johnDoe")
.start();

CaseInstance 启动后,引擎将决定模型中的哪些计划项应该被激活:

  • 第一个阶段没有进入准则,因此它被激活。
  • 第一个阶段的子人工任务没有进入准则,因此预期其中三个处于活动状态。

计划项在运行时由 PlanItemInstance 表示,并可以通过 CmmnRuntimeService 查询:

List<PlanItemInstance> planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery()
.caseInstanceId(caseInstance.getId())
.orderByName().asc()
.list();

for (PlanItemInstance planItemInstance : planItemInstances) {
System.out.println(planItemInstance.getName());
}

输出如下:

After starting
Agree start date
Allocate office
Create email address
Prior to starting
Reject job
Send joining letter to candidate

这里可能有一些出乎意料的地方:

  • 阶段也是计划项,因此也有对应的 PlanItemInstance 表示。注意,子计划项实例在调用 .getStageInstanceId() 时会以该阶段作为父级。
  • 结果中返回了 Send joining letter to candidate。原因在于,按照 CMMN 1.1 规范,该计划项实例处于可用状态,但尚未进入活动状态。

的确,当把上面的代码改为:

for (PlanItemInstance planItemInstance : planItemInstances) {
System.out.println(planItemInstance.getName()
+ ", state=" + planItemInstance.getState()
+ ", parent stage=" + planItemInstance.getStageInstanceId());
}

输出现在变为:

After starting, state=available, parent stage=null
Agree start date, state=active, parent stage=fe37ac97-b016-11e7-b3ad-acde48001122
Allocate office, state=active, parent stage=fe37ac97-b016-11e7-b3ad-acde48001122
Create email address, state=active, parent stage=fe37ac97-b016-11e7-b3ad-acde48001122
Prior to starting, state=active, parent stage=null
Reject job, state=active, parent stage=fe37ac97-b016-11e7-b3ad-acde48001122
Send joining letter to candidate, state=available, parent stage=fe37ac97-b016-11e7-b3ad-acde48001122

要只显示活动的计划项实例,可以在查询中加上 planItemInstanceStateActive():

 List<PlanItemInstance> planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery()
.caseInstanceId(caseInstance.getId())
.planItemInstanceStateActive()
.orderByName().asc()
.list();

现在输出为:

Agree start date
Allocate office
Create email address
Prior to starting
Reject job

当然,PlanItemInstance 是底层表示,但每个计划项还有一个计划项定义,定义了它的类型。在本例中,我们只有人工任务。可以通过用例实例的计划项实例与其交互,例如以编程方式触发它们(如 CmmnRuntimeService.triggerPlanItemInstance(String planItemInstanceId))。然而,大多数情况下,交互会通过实际计划项定义的结果来进行:在这里就是人工任务。

查询任务的方式与 BPMN 引擎完全相同(事实上,任务服务是一个共享组件,在 BPMN 或 CMMN 中创建的任务都可以通过两个引擎查询):

CmmnTaskService cmmnTaskService = cmmnEngine.getCmmnTaskService();
List<Task> hrTasks = cmmnTaskService.createTaskQuery()
.taskCandidateGroup("hr")
.caseInstanceId(caseInstance.getId())
.orderByTaskName().asc()
.list();
for (Task task : hrTasks) {
System.out.println("Task for HR : " + task.getName());
}

List<Task> employeeTasks = cmmnTaskService.createTaskQuery()
.taskAssignee("johndoe")
.orderByTaskName().asc()
.list();
for (Task task : employeeTasks) {
System.out.println("Task for employee: " + task);
}

输出:

Task for HR : Agree start date
Task for HR : Allocate office
Task for HR : Create email address

Task for employee: Reject job

当 HR 的三个任务完成后,'Send joining letter to candidate' 任务应该变为可用:

for (Task task : hrTasks) {
cmmnTaskService.complete(task.getId());
}

hrTasks = cmmnTaskService.createTaskQuery()
.taskCandidateGroup("hr")
.caseInstanceId(caseInstance.getId())
.orderByTaskName().asc()
.list();

for (Task task : hrTasks) {
System.out.println("Task for HR : " + task.getName());
}

的确,预期的任务现在被创建了:

Task for HR : Send joining letter to candidate

完成该任务后,用例实例将进入第二阶段,因为第一阶段的哨兵已满足。'Reject job' 任务由系统自动完成,同时为员工创建两个任务:

Task for employee: Fill in paperwork
Task for employee: New starter training
Task for employee: Reject job

完成所有任务后,用例实例结束:

List<Task> tasks = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).listPage(0, 1);
while (!tasks.isEmpty()) {
cmmnTaskService.complete(tasks.get(0).getId());
tasks = cmmnTaskService.createTaskQuery()
.caseInstanceId(caseInstance.getId())
.listPage(0, 1);
}

在执行用例实例时,引擎还会存储历史信息,可以通过查询 API 查询:

CmmnHistoryService cmmnHistoryService = cmmnEngine.getCmmnHistoryService();
HistoricCaseInstance historicCaseInstance = cmmnHistoryService.createHistoricCaseInstanceQuery()
.caseInstanceId(caseInstance.getId())
.singleResult();

System.out.println("Case instance execution took "
+ (historicCaseInstance.getEndTime().getTime() - historicCaseInstance.getStartTime().getTime()) + " ms");

List<HistoricTaskInstance> historicTaskInstances = cmmnHistoryService.createHistoricTaskInstanceQuery()
.caseInstanceId(caseInstance.getId())
.orderByTaskCreateTime().asc()
.list();

for (HistoricTaskInstance historicTaskInstance : historicTaskInstances) {
System.out.println("Task completed: " + historicTaskInstance.getName());
}

输出:

Case instance execution took 149 ms
Task completed: Reject job
Task completed: Agree start date
Task completed: Allocate office
Task completed: Create email address
Task completed: Send joining letter to candidate
Task completed: New starter training
Task completed: Fill in paperwork

与用例执行相关的历史数据会为各类特殊构造收集,例如任务(如上所见)、里程碑、用例、变量以及一般的计划项。这些数据与运行时数据同时持久化,但在用例实例结束时不会被删除。对历史数据的访问由 CmmnHistoryService 以查询 API 的形式提供。

当然,这只是 Flowable CMMN 引擎中可用的 API 和构造的一小部分。更详细的信息请查阅其他小节。

CMMN 1.1 构造​

本章介绍 Flowable 支持的 CMMN 1.1 构造,以及对 CMMN 1.1 标准的扩展。

以下构造(哨兵和项控制除外)按照 CMMN 规范都被视为计划项。其实例执行的历史数据可以通过 CmmnHistoryService 使用 org.flowable.cmmn.api.history.HistoricPlanItemInstanceQuery 查询。

阶段​

阶段用于把计划项分组到一起。它通常用于定义用例实例中的"阶段"。

阶段本身也是一个计划项,因此可以有进入和退出准则。包含在某个阶段内的计划项只有在父阶段转移到活动状态时才可用。阶段可以嵌套在其他阶段中。

阶段可视化为一个斜角矩形:

cmmn.stage

任务​

一个"手动"任务,意味着该任务在引擎外部发生。

属性:

  • name:在运行时解析为手动任务名称的表达式。
  • blocking:一个布尔值,决定该任务是否阻塞。
  • blockingExpression:一个求值为布尔值的表达式,指示该任务是否阻塞。

如果任务是非阻塞的,引擎在执行它时会自动完成它。如果任务是阻塞的,该任务的 PlanItemInstance 将保持活动状态,直到通过 CmmnRuntimeService.triggerPlanItemInstance(String planItemInstanceId) 方法以编程方式触发它。

任务可视化为一个圆角矩形:

cmmn.task

人工任务​

人工任务用于建模需要由人完成的工作,通常通过表单完成。当引擎执行到人工任务时,会在分配给该任务的所有用户或组的任务列表中创建一个新条目。

人工任务是一个计划项,这意味着除了人工任务条目之外,还会创建一个 PlanItemInstance,可以通过 PlanItemInstanceQuery 查询。

人工任务可以通过 org.flowable.task.api.TaskQuery API 查询。历史任务数据可以通过 org.flowable.task.api.history.HistoricTaskInstanceQuery 查询。

属性:

  • name:在运行时解析为人工任务名称的表达式。
  • blocking:一个布尔值,决定该任务是否阻塞。
  • blockingExpression:一个求值为布尔值的表达式,指示该任务是否阻塞。
  • assignee:一个表达式(可以是静态文本值),用于确定人工任务分配给谁。
  • owner:一个表达式(可以是静态文本值),用于确定人工任务的所有者是谁。
  • candidateUsers:一个表达式(可以是静态文本值),解析为逗号分隔的字符串列表,用于确定哪些用户是该人工任务的候选办理人。
  • candidateGroups:一个表达式(可以是静态文本值),解析为逗号分隔的字符串列表,用于确定该任务分配给哪些组。
  • form key:一个表达式,用于在使用表单时确定一个 key。之后可以通过 API 获取。
  • Due date:一个表达式,解析为 java.util.Date 或 ISO-8601 日期字符串。
  • Priority:一个解析为整数的表达式。可在 TaskQuery API 中用于过滤任务。

人工任务可视化为一个圆角矩形,左上角有一个用户图标:

cmmn.humantask

Java 服务任务​

服务任务用于执行自定义逻辑。

自定义逻辑放在一个实现了 org.flowable.cmmn.api.delegate.PlanItemJavaDelegate 接口的类中。

public class MyJavaDelegate implements PlanItemJavaDelegate {

public void execute(DelegatePlanItemInstance planItemInstance) {
String value = (String) planItemInstance.getVariable("someVariable");
...
}

}

对于无法通过 PlanItemJavaDelegate 方式覆盖的更底层实现,可以使用 CmmnActivityBehavior(类似于 BPMN 引擎中的 JavaDelegate 与 ActivityBehavior)。

属性:

  • name:服务任务的名称。
  • class:实现自定义逻辑的 Java 类。
  • class fields:调用自定义逻辑时传入的参数。
  • Delegate expression:一个表达式,解析为实现了 PlanItemJavaDelegate 接口的类。

服务任务可视化为一个圆角矩形,左上角有一个齿轮图标:

cmmn.servicetask

外部 Worker 任务​

说明​

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

定义外部 Worker 任务​

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

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

用法示例​

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

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

获取外部 Worker 作业​

外部 Worker 作业通过 CmmnManagementService#createExternalWorkerJobAcquireBuilder 使用 ExternalWorkerJobAcquireBuilder 获取。

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

使用上面的 Java 片段可以获取外部 Worker 作业。通过该片段,我们做了以下事情:

  • 查询 topic 为 orderService 的外部 Worker 作业。
  • 获取并锁定作业 30 分钟,等待外部 Worker 发出完成信号。
  • 最多获取 5 个作业。
  • 作业的所有者是 id 为 orderWorker-1 的 Worker。

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

完成外部 Worker 作业​

外部 Worker 作业通过 CmmnManagementService#createCmmnExternalWorkerTransitionBuilder(String, String) 使用 CmmnExternalWorkerTransitionBuilder 完成。

cmmnManagementService.createCmmnExternalWorkerTransitionBuilder(acquiredJob.getId(), "orderWorker-1")
.variable("orderStatus", "COMPLETED")
.complete();

作业只能由获取它的 Worker 完成。否则将抛出 FlowableIllegalArgumentException。

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

也可以使用 CmmnExternalWorkerTransitionBuilder#terminate() 来终止外部 Worker 作业。

外部 Worker 作业的错误处理​

ExternalWorkerJobFailureBuilder 用于让作业失败(将其调度为将来重新执行)。

要让作业失败,可以使用以下方式:

cmmnManagementService.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();

该片段将完成以下事情:

  • 错误消息和错误详情会设置到作业上。
  • 作业的重试次数会设置为 4。
  • 该作业将在 1 小时后可被获取。

作业只能由获取它的 Worker 标记为失败。如果未设置重试次数,Flowable 会自动将作业的重试次数减 1。当重试次数为 0 时,该作业将被移入 DeadLetter 作业表,不再可被获取。

查询外部 Worker 作业​

外部 Worker 作业通过 ExternalWorkerJobQuery 查询,该查询由 CmmnManagementService#createExternalWorkerJobQuery 创建。

决策任务​

决策任务调用一个 DMN 决策表,并把结果变量存储在用例实例中。

属性:

  • Decision table reference:需要调用的、被引用的 DMN 决策表。

还可以通过设置'未命中规则时抛出错误'属性,在 DMN 决策表求值期间没有命中任何规则时抛出错误。

决策任务可视化为一个任务,左上角有一个表格图标:

cmmn.decisiontask

HTTP 任务​

HTTP 任务是 服务任务 的开箱即用实现。当需要通过 HTTP 调用 REST 服务时使用它。

HTTP 任务有多种选项来定制请求和响应。所有配置选项的详细信息请参见 BPMN HTTP 任务文档。

HTTP 任务可视化为一个任务,左上角有一个火箭图标:

cmmn.httptask

脚本任务​

类型为 "script" 的任务,与 BPMN 中的对应物类似,脚本任务在计划项实例变为活动时执行一段脚本。

属性:

  • name:指示任务名称的任务属性。
  • type:任务属性,其值必须为 "script",以指示任务类型。
  • scriptFormat:扩展属性,指示脚本语言(例如 javascript、groovy)。
  • script:要执行的脚本,在名为 "script" 的 field 元素中以字符串定义。
  • autoStoreVariables:可选的任务属性标志(默认:false),指示脚本中定义的变量是否会存储到计划项实例上下文中(参见下面的注意事项)。
  • resultVariableName:可选的任务属性,存在时会把脚本求值结果以指定名称的变量存储到计划项实例上下文中(参见下面的注意事项)。

脚本任务可视化为一个任务,左上角有一个脚本图标:

cmmn.scripttask

<planItem id="scriptPlanItem" name="Script Plan Item" definitionRef="myScriptTask" />
<task name="My Script Task Item" flowable:type="script" flowable:scriptFormat="JavaScript">
<documentation>Optional documentation</documentation>
<extensionElements>
<flowable:field name="script">
<string>
sum = 0;
for ( i in inputArray ) {
sum += i;
}
</string>
</flowable:field>
</extensionElements>
</task>

注意: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' 实际上是一个用例变量(一个整数数组)。

<flowable:field name="script">
<string>
sum = 0
for ( i in inputArray ) {
sum += i
}
</string>
</flowable:field>

注意:也可以在脚本中设置计划项实例变量,只需调用 planItemInstance.setVariable("variableName", variableValue) 即可。默认情况下,不会自动存储任何变量。通过把脚本任务上的 autoStoreVariables 属性设置为 true,可以自动存储脚本中定义的任何变量(例如上面例子中的 sum)。然而,最佳实践是不要这样做,而是显式调用 planItemInstance.setVariable(),因为在某些较新版本的 JDK 中,自动存储变量对某些脚本语言并不奏效。更多细节请参见此链接。

<task name="Script Task" flowable:type="script" flowable:scriptFormat="groovy" flowable:autoStoreVariables="false">

该参数的默认值为 false,意味着如果脚本任务定义中省略该参数,所有声明的变量只在脚本执行期间存在。

下面是在脚本中设置变量的示例:

<flowable:field name="script">
<string>
def scriptVar = "test123"
planItemInstance.setVariable("myVar", scriptVar)
</string>
</flowable:field>

以下名称是保留字,不能用作变量名:out、out:print、lang:import、context、elcontext。

注意 脚本任务的返回值可以通过将其名称作为字面值指定给脚本任务定义的 'flowable:resultVariable' 属性,赋值给一个已存在或新建的计划项实例变量。某个计划项实例变量的任何已有值都会被脚本执行的结果值覆盖。当未指定结果变量名时,脚本结果值会被忽略。

<task name="Script Task" flowable:type="script" flowable:scriptFormat="groovy" flowable:resultVariable="myVar">
<flowable:field name="script">
<string>#{echo}</string>
</flowable:field>
</task>

在上面的例子中,脚本执行的结果(解析后表达式 '#{echo}' 的值)会在脚本完成后设置到名为 'myVar' 的流程变量中。

里程碑​

里程碑用于标记到达用例实例中的某个特定节点。在运行时,它们表示为 MilestoneInstance,可以通过 CmmnRuntimeService 的 MilestoneInstanceQuery 查询。通过 CmmnHistoryService 还有对应的历史查询。

里程碑是一个计划项,这意味着除了里程碑条目之外,还会创建一个 PlanItemInstance,可以通过 PlanItemInstanceQuery 查询。

属性:

  • name:一个表达式或静态文本,用于确定里程碑的名称。

里程碑可视化为一个圆角矩形(比任务的圆角更大):

cmmn.milestone

用例任务​

用例任务用于在另一个用例的上下文中启动一个子用例。CaseInstanceQuery 提供 parent 选项来查找这些用例。

当用例任务是阻塞的时,PlanItemInstance 将保持活动状态,直到子用例完全结束。如果用例任务是非阻塞的,子用例启动后计划项实例自动完成。子用例实例结束时对父用例没有影响。

属性:

  • name:一个表达式或静态文本,用于确定名称。
  • blocking:一个布尔值,决定该任务是否阻塞。
  • blockingExpression:一个求值为布尔值的表达式,指示该任务是否阻塞。
  • Case reference:用于启动子用例实例的用例定义的 key。可以是表达式。

用例任务可视化为一个圆角矩形,左上角有一个用例图标:

cmmn.casetask

流程任务​

流程任务用于在用例的上下文中启动一个流程实例。

当流程任务是阻塞的时,PlanItemInstance 将保持活动状态,直到流程实例完全结束。如果流程任务是非阻塞的,流程实例启动后计划项实例自动完成。流程实例结束时对父用例没有影响。

属性:

  • name:一个表达式或静态文本,用于确定名称。
  • blocking:一个布尔值,决定该任务是否阻塞。
  • blockingExpression:一个求值为布尔值的表达式,指示该任务是否阻塞。
  • Process reference:用于启动流程实例的流程定义的 key。可以是表达式。

流程任务可视化为一个圆角矩形,左上角有一个箭头图标:

cmmn.processtask

流程任务可以配置输入和输出参数,形式为 source/sourceExpression 和 target/targetExpression。

输入参数在用例实例的上下文中解析。

  • source 值将是用例实例变量,其值会映射到一个流程变量。
  • 或者,sourceExpression 允许创建任意值,表达式针对用例实例解析。
  • target 将是源值映射到的流程变量的名称。
  • 或者,targetExpression 会解析为一个字符串值,用作流程实例中的变量名。该表达式在用例实例上下文中解析。

输出参数在流程实例的上下文中解析。

  • source 值将是流程实例变量,其值会映射到一个用例变量。
  • 或者,sourceExpression 允许创建任意值,表达式针对流程实例解析。
  • target 将是源值映射到的用例变量的名称。
  • 或者,targetExpression 会解析为一个字符串值,用作用例实例中的变量名。该表达式在流程实例上下文中解析。

准则​

进入准则(进入哨兵)​

进入准则为给定的计划项实例构成一个哨兵。它们由两部分组成:

  • 一个或多个依赖其他计划项的部分:这些部分定义了对其他计划项状态转换的依赖。例如,一个人工任务可以依赖其他三个人工任务的 'complete' 状态转换,才能自身变为活动状态。
  • 一个可选的 if 部分或条件:这是一个表达式,允许定义复杂条件。

当哨兵的所有准则都解析为 true 时,该哨兵即被满足。当某个准则求值为 true 时,该结果会被存储并在以后的求值中记住。注意,每当用例实例中发生变化时,所有处于可用状态的计划项实例的进入准则都会被求值。一个计划项可以有多个哨兵。不过,当其中一个被满足时,计划项会从可用状态转移到活动状态。

更多信息请参见哨兵求值小节。

进入准则可视化为计划项边框上的一个菱形(内部为白色):

cmmn.entrycriteria

退出准则(退出哨兵)​

退出准则为给定的计划项实例构成一个哨兵。它们由两部分组成:

  • 一个或多个依赖其他计划项的部分:这些部分定义了对其他计划项状态转换的依赖。例如,一个人工任务可以依赖到达某个里程碑而自动终止。
  • 一个可选的 if 部分或条件:这是一个表达式,允许定义复杂条件。

当哨兵的所有准则都解析为 true 时,该哨兵即被满足。当某个准则求值为 true 时,该结果会被存储并在以后的求值中记住。注意,每当用例实例中发生变化时,所有处于活动状态的计划项实例的退出准则都会被求值。一个计划项可以有多个哨兵。不过,当其中一个被满足时,计划项会从活动状态转移到退出状态。

更多信息请参见哨兵求值小节。

退出准则可视化为计划项边框上的一个菱形(内部为白色):

cmmn.exitcriteria

除规范之外,Flowable 在退出哨兵上支持额外属性,为退出哨兵触发时如何终止计划项提供了更多灵活性和选项。

exitType​

该属性可用于计划项上的退出哨兵(但不能用于阶段或用例计划模型),用于定义如何退出该计划项。它与重复规则结合使用时特别有意义。一个可能的场景是:你想终止某个重复计划项的活动实例,但稍后随着用例中条件的变化,它可能再次变为可用。使用默认值之外的退出类型即可做到这一点,因为计划项不会被永久终止,而只终止活动实例、或活动且已启用的实例。

可选取值:

  • default:默认退出类型按规范所述工作,它会终止(退出)该计划项以及所有尚未完成的实例。
  • activeInstances:如果选择该退出类型,退出哨兵只终止活动实例,而保留已启用、可用的实例,以便它们之后可以变为活动状态。
  • activeAndEnabledInstances:除上一种之外,该退出类型还会终止已启用的实例(例如已就绪待手动激活),但保留可用的实例。

人工任务上扩展退出哨兵的示例:

<planItem id="planItem1" name="Task 1" definitionRef="humanTask1">
<itemControl>
<repetitionRule></repetitionRule>
</itemControl>
<exitCriterion id="exitCriterion1" sentryRef="sentry1" flowable:exitType="activeAndEnabledInstances"></exitCriterion>
</planItem>
exitEventType​

该属性可用于阶段或用例计划模型上的退出哨兵,因为它提供了终止之外的另一种退出方式。设想这样一个阶段:你不想为其开启自动完成,而是希望在阶段可完成时有一个用户监听器变为可用,让用户通过触发阶段上的退出哨兵来决定阶段何时真正完成。按照规范使用这种组合会退出该阶段、使其处于终止状态,并触发退出事件以做进一步处理。这可能不是你想要的。使用 exitEventType,你可以指定阶段如何以默认行为之外的方式退出。

可选取值:

  • exit:这是符合规范的默认行为。它会终止该阶段及其所有子元素,并使其处于 terminated 状态,触发的事件类型为 exit。
  • complete:该取值可用于终止阶段,但使其处于 completed 状态(而非 terminated),并触发 complete 事件而非 exit 事件。本质上,该行为与阶段被自动完成完全相同。如果在触发带此退出事件类型的退出哨兵时阶段尚不可完成,引擎将抛出异常。
  • forceComplete:该取值与 complete 类似,但不预先检查阶段是否可完成,而是强制其完成,即使在触发退出哨兵时仍有活动的子计划项。这些子计划项会先被终止,然后阶段以 complete 事件完成并处于 completed 状态。

下面是一个完整示例,展示如何将退出事件类型属性与用户监听器结合使用来手动完成阶段。它包含两个重要部分:退出准则上的 flowable:exitEventType="complete" 属性,以及用户事件监听器上的 flowable:availableCondition="${cmmn:isStageCompletable()}",后者使监听器仅在阶段当前可完成时才可用,否则不可用。一旦用户监听器被触发,退出哨兵就会执行并完成该阶段——不是终止它,而是使其处于 completed 状态,触发的是 complete 事件而非 exit 事件。

下面是该 CMMN 模型的 XML:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:flowable="http://flowable.org/cmmn"
xmlns:cmmndi="http://www.omg.org/spec/CMMN/20151109/CMMNDI"
xmlns:dc="http://www.omg.org/spec/CMMN/20151109/DC"
xmlns:di="http://www.omg.org/spec/CMMN/20151109/DI"
xmlns:design="http://flowable.org/design"
targetNamespace="http://flowable.org/cmmn">
<case id="stageWithUserListenerForCompletion" name="Stage with user listener for completion">
<casePlanModel id="casePlanModel1" name="Case plan model">
<planItem id="planItem4" definitionRef="expandedStage1">
<exitCriterion id="exitCriterion1" flowable:sentryRef="sentry1" flowable:exitEventType="complete"></exitCriterion>
</planItem>
<sentry id="sentry1">
<planItemOnPart id="sentryOnPart1" sourceRef="planItem3">
<standardEvent>occur</standardEvent>
</planItemOnPart>
</sentry>
<stage id="expandedStage1" name="Stage A">
<planItem id="planItem1" name="Task A" definitionRef="humanTask1"></planItem>
<planItem id="planItem2" name="Task B" definitionRef="humanTask2">
<itemControl>
<repetitionRule></repetitionRule>
<manualActivationRule></manualActivationRule>
</itemControl>
</planItem>
<planItem id="planItem3" name="Complete stage" definitionRef="userEventListener1"></planItem>
<humanTask id="humanTask1" name="Task A"></humanTask>
<humanTask id="humanTask2" name="Task B"></humanTask>
<userEventListener id="userEventListener1" name="Complete stage" flowable:availableCondition="${cmmn:isStageCompletable()}"></userEventListener>
</stage>
</casePlanModel>
</case>
</definitions>

事件监听器​

定时器事件监听器​

当需要在用例模型中捕捉时间的流逝时,使用定时器事件监听器。

定时器事件监听器不是任务,与任务相比它的计划项生命周期更简单:当事件(这里是时间流逝)发生时,定时器会直接从可用转移到完成。

属性:

  • Timer expression:一个表达式,定义定时器何时触发。可选取值如下:
-   An expression resolving to a java.util.Date or org.joda.time.DateTime instance (for example, \_${someBean.calculateNextDate(someCaseInstanceVariable)})

- An ISO8601 date

- An ISO8601 duration String (for example, *PT5H*, indicating the timer should fire in 5 hours from instantiation)

- AN ISO8601 repetition String (for example, R5/PT2H, indicating the timer should fire 5 times, each time waiting 2 hours)

- A String containing a cron expression
  • Start trigger plan item/event:对用例模型中某个计划项的引用,用于触发定时器事件监听器的启动。

注意,与哨兵上的进入/退出准则不同,为定时器事件监听器设置启动触发器在用例模型中没有可视化指示。

定时器事件监听器可视化为一个圆圈,内部有一个时钟图标:

cmmn.timereventlistener

用户事件监听器​

当需要捕捉直接影响用例状态的用户交互(而非通过影响用例中的变量或信息间接影响)时,可以使用用户事件监听器。用户事件监听器的典型用例是 UI 中的按钮,用户点击它来驱动用例实例的状态。当事件被触发时,会抛出一个 Occur 事件,哨兵可以监听该事件。与定时器事件监听器一样,它的生命周期比任务简单得多。

cmmn.usereventlistener

用户事件监听器可以使用 org.flowable.cmmn.api.runtime.UserEventListenerInstanceQuery 查询。该查询可以通过调用 cmmnRuntimeService.createUserEventListenerInstanceQuery() 方法创建。注意,用户事件监听器也是一个计划项实例,这意味着它也可以通过 org.flowable.cmmn.api.runtime.PlanItemInstanceQuery API 查询。

用户事件监听器可以通过调用 cmmnRuntimeService.completeUserEventListenerInstance(id) 方法完成。

通用事件监听器​

通用事件监听器通常用于建模编程式交互(例如某个外部系统调用来改变用例实例中的某些内容)。

cmmn.generic event listener

获取并完成这些事件监听器的 API 在 CmmnRuntimeService 上:

GenericEventListenerInstanceQuery createGenericEventListenerInstanceQuery();
void completeGenericEventListenerInstance(String genericEventListenerInstanceId);

与用户事件监听器类似,该 API 是对 PlanItemInstance 查询和操作的一层包装。这意味着也可以通过常规的 PlanItemInstanceQuery 获取这些数据。

注意,通用事件监听器不是 CMMN 规范的一部分,而是 Flowable 特有的扩展。

事件监听器的自动移除​

引擎会自动检测事件监听器(用户或定时器)何时不再有用。以下面这个用例定义为例:

cmmn.user event listener removal 1

这里,First stage 包含两个人工任务(A 和 B),当 Stop first stage 用户事件触发时,用户可以退出该阶段。然而,当任务 A 和 B 都完成时,该阶段也会自动完成。此时如果再触发该用户事件监听器,已经没有任何东西在监听这个事件了。引擎会检测到这一点并自动终止该用户事件。

相同的机制也适用于被进入哨兵引用的事件监听器:

cmmn.user event listener removal 2

在这个例子中,如果 EventListenerA 被触发,EventListenerB 就会被终止(因为已经没有东西在监听它的发生了)。

或者,当定时器和用户事件监听器混合使用时,先被触发的那个也会导致其他监听器被移除(前提是它们没有在别处被引用):

cmmn.user event listener removal 3

这里,如果用户事件先被触发,定时器就会被移除(反之亦然)。

该检测还会考虑尚未创建的计划项。以下面这个用例定义为例:

cmmn.user event listener removal 4

这里,当为该用例定义启动用例实例时,人工任务 C 尚未创建。只要 C 有一个处于非终态的父阶段,该用户事件监听器就不会被移除,因为这意味着该事件将来仍可能被监听。

可用条件​

所有类型的事件监听器都可以配置一个可用条件:一个守护事件监听器可用状态的表达式。为说明该场景,以下面这个用例定义为例:

cmmn.create condition

用例实例启动时,Stage 1(因为没有进入准则)会立即从可用转移到活动。人工任务 A 也是类似情况。人工任务 B 会从可用转移到已启用,因为它需要手动激活。

正常情况下,事件监听器也会变为可用。事件监听器的生命周期比人工任务等计划项更简单:事件监听器停留在可用状态,直到事件发生。它没有像其他计划项那样的活动状态。这意味着用户可以在启动后触发它,从而退出该阶段。

然而在某些场景下,除非某个条件为真,否则事件监听器不应该可供用户交互(或者在使用定时器事件监听器时定时器不应该启动)。

在上面的例子中,我们希望仅当该阶段不再有任何活动的(或必需的)子项时才创建它。将 availableCondition 设置为 ${cmmn:isStageCompletable()},就会允许创建该事件监听器,并使其立即转移到可用状态。具体到这个模型:当人工任务 A 完成时,Stage 1 变为可完成(因为人工任务 B 是手动激活且非必需的)。这使事件监听器的 availableCondition 变为 true,事件监听器现在就可供用户决定退出该阶段。

注意:这是 Flowable 对 CMMN 规范的特有扩展。没有这个扩展,事件监听器就必须嵌套在一个子阶段内,该子阶段通过监听任务 A 完成的进入准则来保护。

注意:如果这是一个可自动完成的阶段,引擎会在 A 完成时自动完成该阶段。

项控制:重复规则​

用例模型上的计划项可以有一个重复规则:一个用于指示某个计划项需要重复执行的表达式。当未设置表达式但启用了重复(例如在 Flowable 建模器中勾选了复选框),或表达式为空时,默认假定为 true。

可以设置一个可选的重复计数变量,它持有该实例的索引(从 1 开始)。如果未设置,默认变量名为 repetitionCounter。

如果计划项没有任何进入准则,则在计划项完成或终止时对重复规则表达式求值。如果表达式解析为 true,就创建一个新实例。例如,一个重复规则表达式为 ${repetitionCounter < 3} 的人工任务,会创建三个顺序执行的人工任务。

如果计划项有进入准则,行为则不同。重复规则不会在完成或终止时求值,而是在计划项的某个哨兵被满足时求值。当哨兵被满足且重复规则求值为 true 时,就创建一个新实例。

以下面这个例子为例:一个定时器事件监听器后面跟着一个人工任务。该哨兵有一个针对定时器事件监听器 occur 事件的进入准则。注意,在任务上启用并设置重复规则会在矩形底部有一个可视化指示。

cmmn.repeatingtimereventlistener

如果定时器事件监听器是重复的(例如 R/PT1H),occur 事件每小时触发一次。当人工任务的重复规则表达式求值为 true 时,每小时都会创建一个新的人工任务实例。

注意,Flowable 允许使用重复的用户事件监听器和通用事件监听器。这与 CMMN 规范(不允许)相悖,但我们认为这是以更灵活的方式使用事件监听器所必需的(例如建模一个用户可能多次触发某个动作、从而创建任务的用例)。

重复规则:最大实例数属性​

Flowable CMMN 模型为重复规则提供了一个扩展属性,可以对并发活动的计划项实例数量进行更多控制。假设你有一个带重复的计划项和一个带条件的进入哨兵。按照规范,只要条件为 true,这会创建无限多个计划项实例,而这可能不是期望的行为。通过重复规则上的 maxInstanceCount 属性,你可以定义是否允许实例数量不受限制(规范的默认值),或者一次只应有一个实例,或任何特定的最大实例数。如果你开启重复并把 maxInstanceCount 设置为 unlimited,就需要以某种方式控制条件,使其只创建你想要数量的实例,或者将其与 on-part(触发器)结合,只在该触发器触发时才创建新实例。

下面的例子中,用户任务带有重复,并结合进入哨兵和条件,确保一次只创建一个实例:

...
<planItem id="planItem1" name="Task 1" definitionRef="humanTask1">
<itemControl>
<repetitionRule flowable:maxInstanceCount="1"></repetitionRule>
</itemControl>
<entryCriterion id="entryCriterion1" flowable:sentryRef="sentry1"></entryCriterion>
</planItem>
<sentry id="sentry1">
<ifPart>
<condition><![CDATA[${vars:getOrDefault('enableTaskA', false)}]]></condition>
</ifPart>
</sentry>
...

下面的例子中,用户任务带有重复和一个重复条件,用于控制创建的实例数量(本例中将创建 5 个任务):

<planItem id="planItem1" name="Task 1" definitionRef="humanTask1">
<itemControl>
<repetitionRule flowable:counterVariable="repetitionCounter" flowable:maxInstanceCount="unlimited">
<condition><![CDATA[${vars:getOrDefault('repetitionCounter', 0) <= 5}]]></condition>
</repetitionRule>
</itemControl>
</planItem>

重复规则:重复集合变量​

与 BPMN 中的多实例类型类似,在 CMMN 中也可以把重复规则与集合中的项结合使用。重复规则提供了一些扩展属性,用于利用元素集合来创建计划项实例。

以下是可用于基于项集合(列表)控制重复规则的扩展属性列表:

  • collectionVariable:将该属性设置为持有集合的变量名,或一个解析为集合的表达式。
  • elementVariable:如果设置了集合变量,可以选择性地设置一个元素变量,在运行时作为计划项实例的局部变量持有当前元素。
  • elementIndexVariable:如果设置了集合变量,可以选择性地设置一个元素索引变量,在运行时作为计划项实例的局部变量持有索引(从 0 开始)。

集合与重复结合使用时,其求值时机可能略有不同。

如果有 on-part(触发计划项的事件)与集合变量结合,则每当该 on-part 被触发时就对集合求值;如果此时集合为 null 或为空,则不创建计划项实例。

如果 on-part 与 if 部分以及基于集合的重复结合,行为相同;不过 if 部分可能有延迟事件触发处理,意味着计划项会先等待 if 部分被满足,然后才检查集合进行重复。换句话说:集合需要在 if 部分被满足且 on-part 触发时(或之前已触发)就绪,然后集合被求值,并根据其元素启动新的计划项实例。

如果既没有 on-part 也没有 if 部分,则每个求值周期都会对集合变量求值,一旦它不为 null,就用它创建新的计划项实例,即使它是空的。这只会执行一次,然后计划项即被终止。以这种方式对集合使用重复时,请确保集合变量恰好在你想为重复求值的时刻可用,否则就把它与 if 部分结合。

如果 if 部分(但没有 on-part)与用于重复的集合变量结合,求值会等待 if 部分被满足,然后才检查集合变量是否为非 null。当 if 部分被满足且集合不为 null(但可能为空)时,集合就用于重复,然后计划项终止。

下面是重复规则结合集合变量的示例:

<planItem id="planItem1" name="Task (${vars:getOrDefault('item', 'na')} - ${vars:getOrDefault('itemIndex', 'na')})" definitionRef="humanTask1">
<itemControl>
<repetitionRule flowable:counterVariable="repetitionCounter" flowable:collectionVariable="myCollection" flowable:elementVariable="item" flowable:elementIndexVariable="itemIndex"></repetitionRule>
</itemControl>
</planItem>

该例子使用了一个名为 myCollection 的集合变量,并指定了元素变量乃至元素索引变量。它们都被用于计划项实例名称表达式中。由于没有进入哨兵,每个求值周期都会对集合求值,一旦 myCollection 不再为 null,就用它进行重复。

项控制:手动激活规则​

用例模型上的计划项可以有一个手动激活规则:一个用于指示某个计划项需要由最终用户手动激活的表达式。当未设置表达式但启用了手动激活(例如在 Flowable 建模器中勾选了复选框),或表达式为空时,默认假定为 true。

阶段和所有任务类型都可以标记为手动激活。在视觉上,任务或阶段会获得一个"播放"图标(指向右侧的小三角形),表示最终用户需要手动激活它:

cmmn.manual activation

正常情况下,当计划项的哨兵被满足时(或该计划项没有任何哨兵时),计划项实例会自动转移到 ACTIVE 状态。但当设置了手动激活且其求值为 true 时,计划项实例会变为 ENABLED 而非 ACTIVE。顾名思义,其背后的想法是最终用户需要手动激活该计划项实例。一个典型用例是显示一组按钮,列出当前可由最终用户启动的潜在计划项实例。

要启动一个已启用的计划项实例,可以使用 CmmnRuntimeService 的 startPlanItemInstance 方法:

List<PlanItemInstance> enabledPlanItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery()
.caseInstanceId(caseInstance.getId())
.planItemInstanceStateEnabled()
.list();

// ...

cmmnRuntimeService.startPlanItemInstance(planItemInstance.getId());

注意,任务的行为只有在计划项实例转移到 ACTIVE 状态时才会执行。例如,对于人工任务,只有在调用 startPlanItemInstance 方法后才会创建用户任务。

已启用的计划项实例可以转移到 DISABLED 状态:

cmmnRuntimeService.disablePlanItemInstance(planItemInstance.getId());

已禁用的计划项实例可以重新启用:

cmmnRuntimeService.enablePlanItemInstance(planItemInstance.getId());

注意,在判定阶段或用例实例是否结束时,DISABLED 状态被视为"终态"。这意味着当只剩下已禁用的计划项实例时,用例实例就会终止。

项控制:必需规则​

用例模型上的计划项可以有一个必需规则:一个用于指示某个计划项被外层阶段(或计划模型)所必需的表达式。它可以用于指示用例模型中哪些计划项是必须执行的、哪些是可选的。

当未设置表达式但启用了必需规则(例如在 Flowable 建模器中勾选了复选框),或表达式为空时,默认假定为 true。

必需规则与父阶段上的 autoComplete 属性协同工作:

  • 如果阶段的 autoComplete 解析为 false(未设置时的默认值),则所有子计划项实例都必须处于终态(完成、终止等),引擎才会完成该阶段计划项实例。
  • 如果阶段的 autoComplete 解析为 true,则必需规则求值为 true的所有子计划项实例都需要处于终态。如果也没有其他活动的子计划项实例,该阶段会自动完成。

阶段计划项实例有一个 completable 属性,可用于查看完成条件是否已满足。以下面这个简单的阶段为例,假设必需任务的哨兵求值为 true,另一个为 false。这意味着左边的计划项实例处于活动状态,而右边的处于可用状态。

cmmn.completeable stage

对该阶段调用 cmmnRuntimeService.completeStagePlanItemInstance(String stagePlanItemInstanceId) 将不可行(会抛出异常),因为它有一个活动的子计划项实例。当左边的用户任务完成后,就可以调用 completeStagePlanItemInstance 了,因为当前没有活动的子计划项实例。然而,阶段本身不会自动完成,因为右边的用户任务处于可用状态。

如果把上面的阶段改为可自动完成(在阶段底部用一个黑色矩形表示),并把左边的计划项改为必需(用一个感叹号表示),行为就会不同:

cmmn.completeable stage02

  • 如果左边的计划项实例处于活动状态(哨兵为 true),右边不活动(哨兵为 false)。在这种情况下,当左边的用户任务完成时,阶段实例会自动完成,因为它没有活动的子计划项实例,且所有必需的计划项实例都处于终态。
  • 如果左右两个用户任务都处于活动状态(哨兵均为 true):
-   When the left user task is completed, the stage will not autocomplete as there is still a child plan item instance active

- When the right user task is completed, the stage will not autocomplete as the required left child plan item instance is not in an end state
  • 如果左边的计划项实例不活动、右边的活动。在这种情况下,当右边的用户任务完成时,阶段不会自动完成,因为必需的左边用户任务尚未处于终态。它需要变为活动状态并完成,才能完成该阶段。

注意,手动激活规则独立于必需规则工作。例如,给定以下阶段:

cmmn.completeable stage03

这里,用户任务 D 是必需的,用户任务 B 是手动激活的。

  • 如果 D 完成,该阶段会自动完成,因为 B 不是必需的且不活动。
  • 如果 B 也是必需的,那么在该阶段自动完成之前,它需要被手动启动(使用 cmmnRuntimeService.startPlanItemInstance(String planItemInstanceId)),即使 D 在 B 手动启动之前就已完成。

项控制:完成中性规则​

用例模型上的计划项可以有一个完成中性规则:一个用于指示某个计划项相对于其父阶段(或计划模型)的完成是中性的的表达式。在某些场景下,它可以作为比使用必需规则和 autoComplete 更灵活的替代方案,用于指示用例模型中哪些计划项必须执行、哪些可选。

注意,完成中性规则不是 CMMN 1.1 标准,而是 Flowable 特有的扩展。

按照规范,一个带有处于 AVAILABLE 状态计划项的阶段不会完成,除非其 autoComplete 属性设为 true 且该计划项不是必需的。例如,一个哨兵未满足的计划项会一直停留在 AVAILABLE 状态,直到哨兵被满足。这意味着父阶段不会完成,除非把该计划项标记为非必需并把阶段设为 autoComplete。缺点是一旦阶段被标记为自动完成,所有子计划项都需要配置 required 规则,这在某些场景下既繁琐又工作量大。

与 autoComplete-required 机制相反,完成中性规则是"自底向上"工作的:可以单独把某个计划项标记为相对于其父级完成是中性的,而无需标记其他任何计划项。

当同时带有两种规则的计划项两者都求值为 true 时,必需规则优先。

总结如下:

  • 一个被配置为*"完成中性"*的计划项,如果处于 AVAILABLE 状态(例如正在等待进入准则哨兵),将允许阶段自动完成;这意味着该计划项相对于其父阶段的完成求值是中性的。
  • 当满足以下任一条件时,阶段将保持 ACTIVE 状态:
1.  It has at least one plan item in **ACTIVE** state

2. It has at least one plan item with *requiredRule* in **AVAILABLE** or **ENABLE** state

3. It is not marked as *autoComplete* and has at least one plan item in **ENABLED** state (irrespective of its *requiredRule*)

4. It is not marked as *autoComplete* and has at least one plan item in **AVAILABLE** state that is **not** *completionNeutral*
  • 当满足以下条件时,阶段将完成:
1.  It contains no plan items or all child plan items are in a *Terminal* or *Semi-terminal* state (CLOSED, COMPLETED, DISABLED, FAILED)

2. It is not marked as *autoComplete* and all remaining child plan items are in **AVAILABLE** state and are *completionNeutral* and not *required*

3. It is *autoComplete* and all remaining plan items are *not required* in **ENABLED** or **AVAILABLE** state (regardless of its completion neutrality, as required rule gets precedence)

项控制:父级完成规则​

除了完成中性规则(即将被弃用)之外,父级完成规则在判定阶段或用例计划模型是否可完成方面提供了更大的灵活性。自动完成可以在所有必需且活动的工作完成后自动完成阶段,但有时你希望以更细粒度的方式,决定在父级可完成的求值过程中如何对待已有的计划项实例。通过父级完成规则,你可以定义某个计划项在其父级完成求值时的行为。

以下是父级完成规则当前支持的类型列表:

  • default:如果需要符合 CMMN 规范的默认行为,使用此取值。
  • ignore:使用此取值时,在对计划项的父级完成状态求值时,该计划项会被完全忽略。这在你有需要完全忽略的计划项(例如一个用例页面)时特别有用,因为它们对用例执行或阶段完成求值没有影响。
  • ignoreIfAvailable:使用此取值时,只有当计划项处于可用状态时才被忽略;如果它处于活动或已启用状态,则会阻止阶段完成。
  • ignoreIfAvailableOrEnabled:此取值把已启用(等待手动激活)状态也纳入忽略范围,只有活动实例才会阻止阶段完成。
  • ignoreAfterFirstCompletion:此取值适用于这样的场景:你有一个带重复的用户任务,希望确保它至少完成过一次,但此后即使它处于活动状态,也不能阻止其父级完成。
  • ignoreAfterFirstCompletionIfAvailableOrEnabled:与上一种相反,如果你希望计划项在首次完成后、当它处于可用或已启用状态但当前不活动时被忽略,使用此取值。

下面是一个对计划项使用父级完成规则的示例。该例子将它与重复规则、必需规则甚至手动激活结合使用。这样,如果该计划项从未被启动并完成过至少一次,它会阻止其父级完成;但如果它已经完成过一次且此后不再处于活动状态,就不再阻止。

<planItem id="planItem1" name="Task A" definitionRef="humanTask1">
<itemControl>
<extensionElements>
<flowable:parentCompletionRule type="ignoreAfterFirstCompletionIfAvailableOrEnabled" />
</extensionElements>
<repetitionRule></repetitionRule>
<requiredRule></requiredRule>
<manualActivationRule></manualActivationRule>
</itemControl>
</planItem>

哨兵求值​

哨兵在任何用例定义中都扮演重要角色,因为它们提供了一种强大的方式,可以以声明式配置的方式,决定某个计划项实例何时激活、何时自动停止。因此,Flowable CMMN 引擎核心逻辑中最重要的部分之一,就是对哨兵求值,以判断用例实例中会发生哪些状态变化。

何时对哨兵求值?​

当用例实例中发生状态变化或出现新事件时,就会对哨兵求值。具体来说包括:

  • 用例实例启动时。
  • 等待状态的计划项(如人工任务)被触发继续执行时。
  • 与用例实例相关的变量发生变化(新增、更新或删除)时。
  • 计划项实例的状态发生变化时(例如通过 RuntimeService 终止、手动启动某个计划项实例等)。
  • 通过 RuntimeService 的 #evaluateCriteria 方法手动触发时。

只要变化持续发生,引擎就会持续为当前所有活动的哨兵安排新的求值。例如,假设某个人工任务的完成满足了另一个人工任务的退出哨兵。第二个人工任务的状态变化会再次安排一次针对所有活动哨兵的新求值,并带上这个新信息。当最近一次求值期间没有发生任何变化时,引擎认为状态已稳定,求值即停止。

概念​

哨兵由两部分组成:

  • 一个或多个 onPart,引用其他计划项的生命周期事件。
  • 零个或一个带有条件的 ifPart。

以下面这个用例定义为例:

cmmn.sentry eval 01

假设(图中未画出):

  • 任务 C 的进入哨兵监听任务 A 和任务 B 的 complete 事件。
  • 退出哨兵监听用户事件监听器 'Stop C' 的 occur 事件。
  • 进入哨兵设置了一个条件表达式 ${var:eq(myVar, 'hello world')}。

在这个简单例子中,进入哨兵有两个 onPart 和一个 ifPart。退出哨兵只有一个 onPart。

用例实例启动时,会创建人工任务 A 和 B(因为它们没有进入哨兵),并立即转移到 active 状态。C 并不活动,而是可用,因为哨兵尚未被满足。用户事件监听器 'Stop C' 从一开始就处于可用状态,因此可以被触发。

当任务 A 和任务 B 都已完成,且变量 myVar 被设置为 'hello world' 时,进入哨兵被满足并触发。C 背后的计划项实例转移到 active 状态,并连带创建出人工任务 C(此后就可以通过 TaskService 查询它)。当 'Stop C' 被触发时(通过 CmmnRuntimeService 的 #completeUserEventListenerInstance 方法),C 的退出哨兵被满足,C 被终止。

如果 'Stop C' 在 C 转移到 active 之前就被触发,它的计划项实例会被终止,进入哨兵也就不再监听任何东西了。

默认行为​

用例实例启动时:

CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("myCase")
.start();

进入哨兵上的条件会立即被求值,因为用例实例启动时会执行一次常规求值周期。

注意,如果像 ${myVar == 'hello world'} 这样使用条件表达式,是行不通的。引擎会抛出 PropertyNotFound 异常,因为它还不知道 myVar 变量。

解决办法:

  • 在用例实例启动时为 myVar 传入一个变量值。
  • 在表达式中做空值检查,例如 ${planItemInstance.getVariable('myVar') != null && planItemInstance.getVariable('myVar') == 'hello world'}。
  • 或者(可能最简单的方式),查阅表达式函数,使用 ${var:eq(myVar, 'hello world')} 这样的函数,它会考虑变量可能不存在这一情况。

默认的求值逻辑是有"记忆"的,这意味着当哨兵的某一部分被满足时,引擎会在后续求值中存储并"记住"这一点。

这意味着,从哨兵的某个部分(onPart 或 ifPart)被满足的那一刻起,该部分在之后的求值中不再被求值,并被视为 true。

在上面的例子中,这样做是必要的,因为任务 A 通常会在与任务 B 不同的时间点完成。例如,如果任务 A 先完成,那么任务 C 的哨兵中"我在监听任务 A 的 complete 事件"这一部分现在就被满足了,这一事实会被记住供以后使用。如果此时 B 完成了,这一点也会被存储。如果此时 myVar 变量获得了正确的值,ifPart 也会触发,整个哨兵随之触发,任务 C 被激活。当然,也有可能变量值先被满足、任务后完成。关键在于这并不重要,因为引擎会记住过去已满足的部分。

这种"带记忆"的行为是引擎的默认行为,通过把哨兵的 triggerMode 设置为 default 来实现。在 Flowable 建模器中添加新计划项时会自动设置它。当未设置任何值时(例如从其他工具导入用例模型时),triggerMode 默认为 default。

触发模式 "onEvent"​

默认行为(见上一节)会记住哪些部分在之前已被满足。这是最常用、最安全的方式(也是人们思考哨兵时通常的预期方式)。

哨兵触发还有另一种模式,称为 "onEvent"。在这种模式下,引擎对哨兵的各部分不会记住过去已满足的任何部分。在高级场景中有时需要这样。以下面的例子为例:

cmmn.sentry eval 02

这里,用例模型有一个阶段,下面包含三个子阶段。所有子阶段都是重复的。子阶段 B 和 C 都有一个进入哨兵,用于监听阶段 B 的完成。此外(图中未画出),两个哨兵都有一个依赖于某个变量的条件。

在高级场景中,可能希望或需要哨兵各部分(尤其是包含条件的 ifPart)只在被依赖计划项的生命周期事件发生时才求值。在本例中,这个事件就是 Stage A 的 complete 事件。对于这类场景,可以把哨兵的 triggerMode 设置为 onEvent。顾名思义,这意味着哨兵求值只在引用的事件发生时进行,不考虑任何过去的记忆。

具体到本例,进入哨兵的条件只在 Stage A 完成时才会被求值(其他任何时刻都不会)。这与一般的求值规则非常不同。在这个特定例子中,它确实让变量管理变得更容易,因为条件只在一个精确的时刻求值,不必担心某个哨兵部分因为变量在某个时间点有值而被触发。尤其本例中所有子阶段都是重复的,否则管理起来工作量会很大。这是一个强大的机制,但它面向的是对用例模型和该 triggerMode 语义有深入理解的高级建模者。

请注意,在对哨兵求值时,引擎会认为所有事件是同时发生的。以下面这个用例定义为例:

cmmn.sentry eval 03

假设所有哨兵都使用 triggerMode onEvent 设置。如果任务 A 完成,会退出任务 B。任务 C 此时也会退出。所以,尽管这里有两个不同的生命周期事件(A 完成和 B 退出),人们可能会以为 onEvent 字面意思是发生两次独立求值、任务 C 退出哨兵另一部分的记忆被遗忘,但引擎足够聪明,能识别出它们属于同一个求值周期,任务 C 也会随之退出。

从技术上说:onEvent 哨兵有一些记忆,具体来说,是对同一次 API 调用(或从底层说,同一个事务)中发生的求值有记忆。

重要提示:onEvent 是一个强大的机制,只有在充分理解其语义时才应使用。如果场景没有经过仔细推敲,完全可能因为哨兵配置不正确而创建出一个卡住不动的用例模型。

(例如,假设一个哨兵有一个监听某计划项完成的 onPart,以及一个带条件的 ifPart。如果该计划项完成了——从而触发了 onPart——但条件中使用的某个变量因某种原因缺失……那么 ifPart 将永远不会触发,用例实例就可能卡在不想要的状态里)。