一、简介
本文仅是记录个人使用 Flowable Case Model 过程中的情况;接下来的内容是关于如何定义和部署一个案例模型定义的内容。详细文档可以查阅 Flowable 官方文档 哈!
二、认识案例模型(Case Model)
简述
Flowable支持通过 Spring Boot 集成进行二次开发的,它由配置信息(CmmnEngineConfiguration)来创建引擎(CmmnEngine)
一样是由 org.flowable.cmmn.spring.CmmnEngineFactoryBean 工厂来创建出来的,你也可以自定义修改配置。
CmmnEngine提供五种内置服务,它们都是由Spring管理的Bean,所以可以直接引用 @Autowired 或使用构造函数。
CmmnRuntimeService runtimeService = cmmnEngine.getCmmnRuntimeService();
CmmnRepositoryService repositoryService = cmmnEngine.getCmmnRepositoryService();
CmmnTaskService taskService = cmmnEngine.getCmmnTaskService();
CmmnManagementService managementService = cmmnEngine.getCmmnManagementService();
CmmnHistoryService historyService = cmmnEngine.getCmmnHistoryService();
- 我们在设计案例模型定义的过程中每一个功能节点,你都可以把看做是一个计划项,
- 计划项会指向一个计划项定义,它可以由一个哨兵来控制进入和退出,哨兵相当于一个守卫者,想要执行计划项需要满足它的条件。
- 计划项的类别有哪些? 如图所示
- 每一个计划项定义都拥有不同的业务行为,可以通过设定标签上的属性来支配它的行为;例如用户任务的是否手动激活
<planitem id="planItem1" name="OneTask" definitionref="OneTask">
<itemcontrol>
<manualactivationrule></manualactivationrule>
</itemcontrol>
</planitem>
<humantask id="OneTask" name="OneTask" flowable:assignee="${initiator}" flowable:formfieldvalidation="true">
<extensionelements>
<modeler:flowable-idm-initiator
xmlns:modeler="http://flowable.org/modeler">
</modeler:flowable-idm-initiator>
</extensionelements>
</humantask>
变量
我们运行一个案例实例都是需要通过数据来执行其组成的步骤。在 Flowable 中,这些数据就称为变量,存储在数据库中。变量可以用在表达式中(例如,在哨兵的条件中)、调用外部服务时的 Java 服务任务中等等。
案例实例的变量通称为案例变量,而且计划项实例和人工任务也可以拥有变量。每个案例实例可以有任意数量的变量。它们都存储在ACT_RU_VARIABLE 这张数据库表中。
除此之外,还有一种瞬态变量 就是不持久化的变量,它是出现在案例实例执行的过程中;每个计划项实例的瞬态变量只能在下一个计划项实例的等待状态之前访问到,而如果案例变量和瞬态变量同名的情况下,Flowable在你访问计划项实例变量的时候实际上返回的是瞬态变量。
表达式
Flowable 表达式主要是使用在给变量赋值,可用条件赋值以及执行调用方法上,你可以划分为:值表达式和方法表达式。Flowable 使用 UEL语法进行表达式解析。
- 值表达式
解析为一个值。默认情况下,所有案例变量都可供使用。在Spring环境下也可以直接使用Bean名称
${variable}
${bean.propertyname}
- 方法表达式
当调用不带参数的方法时,请务必在方法名称后面添加空括号(因为这可以区分表达式与值表达式)。
${bean.print()}
${bean.createOrder('orderName')}
${bean.doCreate(myVar, planItemInstance)}
除了自定义的案例实例的变量之外,还有框架定义的默认表达式对象:
- caseInstance:保存有关正在进行的案例实例的附加信息。
- planItemInstance:关键字在所有计划项相关表达式(例如哨兵条件、计划项生命周期侦听器、服务任务表达式等)中均可用。
- planItemInstances:公开有关所有当前计划项实例的信息。
- variableContainer:变量容器是案例实例、计划项实例、流程实例和执行之上的抽象。 +variableContainer 关键字允许编写不绑定到特定实现的表达式。
- authentiatedUserId:当前经过身份验证的用户的 ID。如果没有用户经过身份验证,则该变量不可用。
示例:
${caseInstance.id}
${caseInstance.getVariable('myVariable') == 'test'}
${caseInstance.setVariable('myVariable', 'test')}
${planItemInstance.getPlanItem().getPlanItemDefinition().getName()}
${planItemInstance.getVariable('myVariable') == 123}
${planItemInstance.setVariable('myVariable', 123)}
${variableContainer.getVariable('myVariable')}
${variableContainer.setVariable('myVariable', 'true')}
# 将返回当前处于“活动”状态的所有计划项实例的计数
${planItemInstances.active().count()}
除了默认的表达式还提供了默认的表达式函数
- Variables:get(varName):获取变量的值
- Variables:getOrDefault(varName, defaultValue) :获取变量的值,但可以选择提供默认值,当变量未设置或值为null时返回该默认值。
- Variables:exists(varName) :判断变量具有非空值,则返回true
- Variables:isEmpty(varName)(简写:empty):检查变量值是否不为空。根据变量类型,行为如下:
- 对于字符串变量,如果它是空字符串,则该变量被视为空。
- 对于 java.util.Collection 变量,如果集合没有元素,则返回true 。
- 对于 ArrayNode 变量,如果没有元素,则返回true 。
- 如果变量为null,则始终返回true 。
- Variables:isNotEmpty(varName)(简写:notEmpty ): isEmpty的逆操作。
- Variables:equals(varName, value)(简写:eq):判断变量是否等于给定值。
- Variables:notEquals(varName, value)(简写:ne ): equals的反向比较。
- Variables:contains(varName, value1, value2, …):检查提供的所有值是否都包含在变量中。根据变量类型,行为如下:
- 对于字符串变量,传递的值用作需要成为变量一部分的子字符串。
- 对于 java.util.Collection 变量,所有传递的值都需要是集合的元素(常规包含语义)。
- 对于 ArrayNode 变量:支持检查 arraynode 是否包含支持作为变量类型的类型的 JsonNode。
- 当变量值为 null 时,所有情况都返回 false。当变量值不为 null,且实例类型不是上述类型之一时,将返回 false。
- Variables:containsAny(varName, value1, value2, …) :与contains函数类似,但如果变量中包含任何(而不是全部)传递的值,则会返回true 。
- Variables:base64(varName):将二进制或字符串变量转换为 Base64 字符串
- 比较器功能:
- Variables:lowerThan(varName, value)(简写:lessThan或:lt):${execution.getVariable(“varName”) != null && execution.getVariable(“varName”) < value} 的简写。
- Variables:lowerThanOrEquals(varName, value)(别名:lessThanOrEquals或:lte):类似于 <=
- Variables:greaterThan(varName, value)(简写:gt):类似于 >
- Variables:greaterThanOrEquals(varName, value)(别名:gte):类似为 >=
变量命名空间的别名为vars或var。因此,Variables:get(varName)相当于编写vars:get(varName)或var:get(varName)。请注意,不需要在变量名称两边加上引号。
案例定义设计
基本概念和术语
CMMN 没有版本控制的概念,每个案例定义的初始化属性包含键、版本、名称和 ID,假设我们开始定义一个案例定义:
<?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="TestSpaces" xmlns:custom="http://www.w3.org/1999/xhtml"
exporter="Flowable Open Source Modeler" exporterVersion="6.8.0">
<case id="customOrderModel" name="custom Order Model" flowable:initiatorVariableName="initiator">
<casePlanModel id="orderCasePlanModel">
.....
</case>
</definitions>
- XML 文件中的案例定义 id 属性用作案例定义关键属性,是案例定义的唯一标识。如要部署同一个案例定义的更新版本,请保持 id 不变
- XML 文件中的案例定义名称属性用作案例定义名称属性。如果未指定 name 属性,则使用 id 属性作为名称。
- XML 文件中的案例定义命名空间属性是用来区分案例的类别。业务上最好还是设置类别值,将来可以隔离用户之间的业务案例
- XML 文件中定义的标签头都是内置的,假如你想要使用自定义的标签,那么就需要先在这里声明标签,然后在扩展元素标签里面使用它。举个例子:新增一个标签 xmlns:custom=“http://www.w3.org/1999/xhtml”
<?xml version="1.0" encoding="UTF-8"?>
<definitions
省略内置标签....
xmlns:custom="http://www.w3.org/1999/xhtml"
exporter="Flowable Open Source Modeler" exporterVersion="6.8.0">
<case id="customOrderModel" name="custom Order Model" flowable:initiatorVariableName="initiator">
<casePlanModel id="orderCasePlanModel">
<extensionElements>
<custom:form id="custom_order_case_form" name="custom Order Case Form">
<field id="product_name" name="Product Name" type="string"></field>
<field id="quantity" name="quantity" type="number"></field>
<field id="origin_price" name="origin price" type="float"></field>
</custom:form>
</extensionElements>
.....
</case>
</definitions>
上面XML文件中,新增了一个自定义标签custom以及在扩展元素中添加了一个表单信息custom:form 和它的属性列表
如何部署案例定义
在SpringBoot框架环境下,直接使用CMMN引擎Bean调用CmmnRepositoryService服务来发布部署即可
@Autowired private CmmnEngine cmmnEngine;
public CmmnDeployment deploy(MultipartFile multipartFile) {
String fileName = multipartFile.getOriginalFilename();
if (!fileName.endsWith("cmmn.xml")) {
throw new DocPalWorkflowException("Incorrect file format, it should be cmmn.xml file");
}
CmmnRepositoryService cmmnRepositoryService = cmmnEngine.getCmmnRepositoryService();
InputStream inputStream = null;
try {
inputStream = multipartFile.getInputStream();
// Deploy case model
return cmmnRepositoryService.createDeployment()
.addInputStream(multipartFile.getOriginalFilename(), inputStream)
.deploy();
} catch (CmmnXMLException e) {
e.printStackTrace();
}
}
如何启动一个案例定义来得到一个案例实例
在SpringBoot框架环境下,使用CMMN引擎Bean调用CmmnRuntimeService服务来启动一个案例定义
@Autowired private CmmnRuntimeService cmmnRuntimeService;
public CaseInstance start(String caseDefinitionKey, String businessKey, Map<String, Object> parameters) {
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.businessKey(BusinessKey)
.caseDefinitionKey(caseDefinitionKey)
.variables(parameters)
.start();
return caseInstance;
}
请注意,如果你设计的案例定义变量没有设置默认值,那么 parameters 请求参数Map应包含此变量,否则会启动失败。
所以你的表达式最好是使用带默认值的 ${var:getOrDefault(varName, defaultValue)}
添加一个用户任务
假设在案例定义中添加一个用户任务,命名为 Human task A,案例设置自动完成(autoComplete=“true”),如下所示
<?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="TestSpaces" exporter="Flowable Open Source Modeler" exporterVersion="6.8.0">
<case id="customTaskModel" name="custom Task Model" flowable:initiatorVariableName="initiator">
<casePlanModel id="customTaskPlanModel" flowable:formFieldValidation="true" autoComplete="true">
<planItem id="planItem1" name="Human task A" definitionRef="sid-88199E7C-7655-439C-810B-8849FC52D3EB"></planItem>
<sentry id="sentry1">
<planItemOnPart id="sentryOnPart1" sourceRef="planItem1">
<standardEvent>complete</standardEvent>
</planItemOnPart>
</sentry>
<humanTask id="sid-88199E7C-7655-439C-810B-8849FC52D3EB" name="Human task A"></humanTask>
</casePlanModel>
</case>
</definitions>
如果业务场景需要满足人工激活任务的话,可以在计划项添加以下内容
<planItem id="planItem1" name="Human task A" definitionRef="sid-88199E7C-7655-439C-810B-8849FC52D3EB">
<itemControl>
<manualActivationRule></manualActivationRule>
</itemControl>
</planItem>
酱紫的话,即使用户任务计划项实例的满足可用条件是启用状态(enabled),你可调用Java代码来激活计划项实例
cmmnRuntimeService.startPlanItemInstance(planItemInstance.getId());
用户任务计划项实例的状态变化如下:unavailable -> available -> enabled -> active -> completed
这里我再次强调下标签的排版顺序,一定保证先有计划项 -> 再有哨兵 -> 最后指定的计划项定义,不然发布部署会失败
<planItem id="planItem1" .....
<sentry id="sentry1" .......
<humanTask id="sid-88199E7C-7655-439C-810B-8849FC52D3EB" ......
这里是我在当前版本6.8.0使用CmmnRepositoryService服务来发布会出现失败的情况,如您的版本已经升级或者不同,请忽略它。另外XML文件的后缀名最好是保证是 .cmmn.xml
哨兵
哨兵的设置上,任务和事件的触发类型要符合上一个定义的要求。
- 如果是一个任务去触发一个哨兵的话,指向线使用complete类型
- 如果是一个用户事件的话,指向线可以使用occur类型
如果你设定的过渡事件类型不符合出发计划项定义的生命周期,极有可能导致无法使下一个计划项实例启动失败。
阶段-Stage
阶段按照业务来理解相当于一个完整的功能模块,比如说:注册用户的场景,可以划分为
- 注册用户
- 账号登录
- 注销用户
一般理解为三个完整的功能模块,Stage就类似于酱紫拿来划分模块区域的。
阶段的行为属性如图所示
需要注意的是,如果某一个阶段里面设置了多个计划项,理论上需要全部的计划项实例都执行完成或者结束才能退出阶段,否则即使满足阶段的退出条件也不会退出。
案例实例启动之后,阶段计划项实例的状态变化:**available -> enabled -> active -> complete **