Camunda调用子流程(Call Activity)
概述
官方文档在这一章的描述中经常会用到的术语是MainProcess和SubProcess,或者Calling Process和Called Process,我就简单的用“父流程”和“子流程”来代替了,并不严谨,提前知会一下。
BPMN 2.0中有2种类型的子流程,分别是SubProcess(bpmn:subProcess
)和Call Activity(bpmn:callActivity
)。
从概念的角度来看,当流程执行到达这二种活动时(严格的讲,子流程可以细分为:Embedded SubProcess
, Call Activity
, Event SubProcess
和Transactional SubProcess
),两者都会调用子流程。但是BPMN2.0还是对这二个概念进行了区别,不同之处在于Call Activity引用的是流程定义外部的流程,而SubProcess则嵌入在原始流程定义内。
Call Activity的主要使用场景是从其他流程定义中去调用另外一个流程,这可以让一些常用的流程定义得到复用,比如结账流程通常就是可以被别的流程调用的。可以类比成函数调用另外一个工具函数。目的是实现流程的模块化(Modularization)和重用(reuse)。
当流程执行到达Call Activity时,将创建一个新的子流程的流程实例,该实例用于执行子流程,这样就实现了原来主流程实例和新创建的子流程实例并行执行。主流程实例会等着子流程完全结束,然后再继续执行原来流程的后续环节。
在BPMN 2.0中Call Activity的符号是一个加粗的圆角矩形:
这个符号很容易跟那个折叠的嵌入式子流程弄混,区别是Call Activity的边框要更厚一些:
从BPMN XML的角度来看,callActivity元素通过一个必填的calledElement
属性,去引用需要调用的流程定义。也就是Camunda Modeler中的流程id:
注意:子流程的流程定义是在运行时才进行解析的。 这意味着如果有需要的话,子流程可以独立于主流程单独进行部署。而不用纠结谁先谁后。
调用绑定
Call Activity通过指定它的calledElement
属性为流程定义名称,这样就可以引用到要调用的子流程,当只设置这个calledElement
属性时,默认会调用引用到的那个流程的最新版本流程定义。
1 | <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="HR" camunda:calledElementTenantId="demo"> |
为了调用其它版本的子流程,可以通过定义指定Call Activity的以下可选属性:calledElementBinding
, calledElementVersion
, calledElementVersionTag
。
CalledElementBinding
属性有以下4个属性值:
latest
始终调用最新版的流程定义(如果未定义
calledElementBinding
属性,这也是Call Activity的默认行为)。
deployment
如果被调用的流程定义与调用的流程定义属于同一部署(也就是说一个deployment中同时存在调用流程和被调用流程),则使用deployment的版本。
version
调用一个固定版本的流程定义,在这种场景下(即:
calledElementBinding="version"
),calledElementVersion
属性就是必填的。这个属性就是想要调用流程定义的版本号,这个属性的值可以直接“写死”在BPMN XML中或者是通过一个表达式(custom extensions)来返回。1
2// calledElementVersion值类型
java.lang.Integer or org.camunda.bpm.engine.delegate.Expression
versionTag
调用一个固定版本标签的流程定义,在这种场景下(即:
calledElementBinding="versionTag"
),calledElementVersionTag
属性就是必填的。这个属性就是想要调用流程定义的版本号标签,这个属性的值可以直接“写死”在BPMN XML中或者是通过一个表达式(custom extensions)来返回。1
2// calledElementVersionTag值类型
java.lang.String or org.camunda.bpm.engine.delegate.Expression
多租户考虑
默认租户解析
默认情况下,用父流程定义的租户ID去解析子流程定义。
- 如果父流程定义没有配置租户ID,那么子流程使用给定的Key、binding以及没有租户ID(tenantId = null)去解析流程定义。
- 如果父流程定义配置了租户ID,那么子流程使用给定的Key、binding以及和父流程相同的租户ID去解析流程定义。
注意:在默认行为中,父流程实例的租户ID并不在考虑之内。
显式租户解析
在某些情况下,覆盖默认行为并明确指定租户ID可能会很有用。
callActivity元素的calledElementTenantId
属性可以明确的指定租户ID:
1 | <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="HR" camunda:calledElementBinding="versionTag" camunda:calledElementVersionTag="xxx" camunda:calledElementTenantId="demo"> |
如果在设计时不知道租户ID,则也可以使用表达式:
1 | <callActivity id="callSubProcess" calledElement="checkCreditProcess" |
表达式(expression)也允许使用父流程实例的租户ID,而不是使用父流程定义的租户ID:
1 | <callActivity id="callSubProcess" calledElement="checkCreditProcess" |
传递变量
可以将父流程的流程变量传递给子流程,反之亦然。这些变量数据在启动时被复制到子流程中,然后在子流程结束时再复制回主流程。
父流程传递给子流程
选中Call Activity组件之后,在属性面板中切换到Variables标签,点击In Mapping区域右上角的小加号,添加一条记录。
Type
选择Source。Source
输入框中输入父流程的一个变量名称,比如:ageInMainProcess。Target
输入框中输入当父流程的变量被复制到子流程后,子流程要用哪个名字的变量来承接,比如:ageInSubProcess。
BPMN XML:
1 | <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="HR" camunda:calledElementTenantId="demo"> |
启动父流程:
父流程变量:
子流程变量:
更近一步,也可以配置将所有流程变量都传递到子流程,这样就不用逐个设置需要传递的变量名称了。选中Call Activity组件之后,在属性面板中切换到Variables标签,点击In Mapping区域右上角的小加号,添加一条记录。记录中Type
选择all:
BPMN XML:
1 | <bpmn:callActivity id="Activity_1vme4hv" name="IT环节" calledElement="StaffEnrollment_IT" camunda:calledElementTenantId="demo"> |
子流程传递给父流程
选中Call Activity组件之后,在属性面板中切换到Variables标签,点击Out Mapping区域右上角的小加号,添加一条记录。
Type
选择Source。Source
输入框中输入子流程的一个变量名称,比如:bomInSubProcess。Target
输入框中输入当子流程的变量被复制回父流程后,要用哪个名字的变量来承接,比如:bomInSuperProcess。
BPMN XML:
1 | <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="StaffEnrollment_HR" camunda:calledElementTenantId="demo"> |
启动父流程:
父流程变量:
子流程变量:
为子流程添加一个变量(这个变量值会复制回主流程):
子流程结束后,查看父流程的变量:
更近一步,也可以配置将所有流程变量都传递回给父流程,这样就不用逐个设置需要传递的变量名称了。选中Call Activity组件之后,在属性面板中切换到Variables标签,点击Out Mapping区域右上角的小加号,添加一条记录。记录中Type
选择all:
BPMN XML:
1 | <bpmn:callActivity id="Activity_1vme4hv" name="IT环节" calledElement="StaffEnrollment_IT" camunda:calledElementTenantId="demo"> |
表达式
也可以在输入/输出变量这里,使用表达式来进行设置:
1 | <callActivity id="callSubProcess" calledElement="checkCreditProcess" > |
因此,最终z = y + 5 = x + 5 + 5成立。
BPMN Error事件的变量输出
当子流程实例抛出的BPMN错误事件在父流程实例中被捕获时,输出变量映射也会被执行。
根据BPMN的建模不同,要求当错误被传播的时候,输出参数要容许子流程的变量有null值存在。
组合输入/输出参数功能
Call Activity的变量传递功能也可以与输入/输出参数功能(Input/Output parameters)组合起来使用。这样可以更加灵活地将变量映射到子流程中去。
注意:为了只映射声明在inputOutput中的变量,可以使用
local
属性。设置
local="true"
意味着将执行Call Activity的所有局部变量映射到子流程的流程实例中。这些正是定义为Input Parameters的变量(配置在Input parameter中的变量的作用域都是local范围的)。
选中Call Activity组件之后,在属性面板中切换到Input/Output标签,点击Input Parameters区域右上角的小加号,添加一条记录。
Name
输入一个参数名称,比如:var1。Type
选择Script。Script Format
输入框中脚本语言的名称,比如:groovy。Sciprt Type
选择Inline Script。Script
输入域中输入一段脚本内容,用来操作变量或者参数,比如:sum = a + b + c
。
再将属性面板的Tab中切换到Variables,点击In Mapping区域右上角的小加号,添加一条记录。记录中Type
选择all,并勾选最下面的Local
复选框:
BPMN XML:
1 | <bpmn:callActivity id="Activity_0j1gipz" name="部门助理环节" calledElement="StaffEnrollment_DeptAssistant" camunda:calledElementTenantId="demo"> |
最终子流程可以看到的变量:
Name | Type | Value | Scope |
---|---|---|---|
var1 | Integer | 6 | 员工入职_部门助理环节 |
var2 | String | 员工入职_部门助理环节 |
一样的道理,对于输出参数也可以这样做:
1 | <callActivity id="callSubProcess" calledElement="checkCreditProcess" > |
当子流程实例结束之后,由于将camunda:out
元素的local
属性设置为了true,正在执行的子流程中的所有变量都会被映射到Local变量中。
这些变量可以使用Output Parameter Mapping功能再映射到流程实例中。Call Activity结束后,所有未经camunda:outputParameter
元素声明的变量都不再可用。
变量映射委托
输入/输出变量的映射也可以被委托(官方文档中所说的委托,我理解为就是把一部分要实现或者需要自定义的功能,交给用户自己的代码来实现,交给别人来实现也就是所谓的“委托”)。这意味着可以通过Java代码来实现输入/输出变量。为了实现这样的功能,必须实现DelegateVariableMapping接口。
1 | public class DelegatedVarMapping implements DelegateVariableMapping { |
我们可以通过如下2种委托方案实现变量映射:
引用方式(Reference)
设置Camunda的扩展属性variableMappingClass
,指定这个属性的值为一个实现了DelegateVariableMapping
接口的类的全限定类名。
首先选中Call Activity,切换到General
标签,在Delegate Variable Mapping
下拉菜单中选择variableMappingClass,然后在下面的Class
输入框中填写全限定类名称:
BPMN XML:
1 | <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="StaffEnrollment_HR" camunda:calledElementTenantId="demo" camunda:variableMappingClass="me.ningyu.itsm.DelegatedVarMapping"> |
表达式方式(Expression)
设置Camunda的扩展属性variableMappingDelegateExpression
,指定这个属性的值为一个表达式。这里允许指定一个表达式,该表达式可以解析为实现DelegateVariableMapping
接口的对象。
首先选中Call Activity,切换到General
标签,在Delegate Variable Mapping
下拉菜单中选variableMappingDelegateExpression
,然后在下面的Delegate Expression
输入框中填入表达式语句:
BPMN XML:
1 | <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="StaffEnrollment_HR" camunda:calledElementTenantId="demo" camunda:variableMappingDelegateExpression="${expr}"> |
传递BusinessKey
父流程可以传递BusinessKey给子流程。当子流程启动的时候BusinessKey被复制到子流程中去。
注意你无法将BusinessKey返回还给父流程,因为BusinessKey是不可更改的。
首先选中Call Activity,切换到General
标签,勾选中Business Key
,然后会在下方出现一个输入框,输入框中有默认值#{execution.processBusinessKey}
,表示传递父流程实例的BusinessKey作为子流程实例的BusinessKey,你可以直接使用这个默认值,或者手动修改输入框中的值。
注意事项
Source
和Target
输入框中输入的变量名,可以相同,也可以不同。- 如果勾选了In Mapping区域条目下面的
Local
复选框,那么Target
输入框中定义的变量名会出现在子流程中,但是值不会由父流程复制给子流程(即,Type为Null,Value为空)。 - 如果勾选了Out Mapping区域,条目下面的
Local
复选框,那么Target
输入框中定义的变量名不会出现在主流程中。 - 默认情况下,在out元素(camunda:out)中声明的变量,会被设置在尽可能的最高的变量范围内。
- 在子流程的流程实例的上下文中计算源表达式。 这意味着,在父流程定义和子流程定义属于不同的应用程序的情况下,上下文环境( 像Java classes, Spring 或 CDI beans)是从属于子流程定义的程序解析出来的。
- BPMN 1.2中嵌入式的子流程和可重用的子流程都是使用的subProcess元素,用一个属性进行区别。到了BPMN 2.0将这两类子流程分成了2个不同的元素:subProcess和callActivity。
- 嵌入式子流程不能包含泳池和泳道,但是嵌入式子流程可以放在父流程的泳池或泳道之中。
备忘
- 设置变量为任务的局部变量方法:
1 | curl --location --request PUT 'http://11.11.176.126:48086/engine-rest/task/c2f6d18f-596b-11eb-b228-0a58ac0009d7/localVariables/bomInSubProcess' \ |
声明在inputOutput标签页中的参数,它们的作用域只在当前的活动/任务,我们可以这样验证:
选中Call Activity
先切换到Variables标签页,在In Mapping中添加一个
Type
为All的映射类型,注意先不要勾选Local
。再切换到Input/Output标签页,在Input Parameters添加二个输入参数:
变量1
Name:
var1
Type:
Script
Script Format:
groovy
Script Type:
Inline Script
Script:
sum = a + b + c
变量2
Name:
var2
Type:
Text
Value:
Test
然后在Camunda TaskList中通过“Start Process”发起一个流程实例,添加三个流程实例变量:
变量1
- Name:
a
- Type:
Integer
- Value :
1
变量2
- Name:
b
- Type:
Integer
- Value :
2
变量3
- Name:
c
- Type:
Integer
- Value :
3
- Name:
登陆到Camunda Cockpit,在流程实例的详情中可以查看到5个变量:
Name Type Value Scope a Integer 1 员工入职 b Integer 2 员工入职 c Integer 3 员工入职 var1 Integer 6 HR环节 var2 String Test HR环节 从Camunda Cockpit的Called Process Instances标签进入到子流程详情中,可以发现上述的5个变量全都被映射到了子流程中。
回到Camunda Modeler中,切换到Variables标签页,在In Mapping中添加一个Type为All的映射类型,这次勾选
Local
,重新部署流程。参照上面的方法再次发起一个流程实例,添加同样的变量,登陆到Camunda Cockpit,在子流程的流程实例的详情中只可以查看到2个变量:
Name Type Value Scope var1 Integer 6 员工入职_HR环节 var2 String Test 员工入职_HR环节
参考
- Call Activity | docs.camunda.org
- BPMN 2.0 Symbol Reference - Camunda
- Delegation Code | docs.camunda.org