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 SubProcessTransactional SubProcess),两者都会调用子流程。但是BPMN2.0还是对这二个概念进行了区别,不同之处在于Call Activity引用的是流程定义外部的流程,而SubProcess则嵌入在原始流程定义内。

Call Activity的主要使用场景是从其他流程定义中去调用另外一个流程,这可以让一些常用的流程定义得到复用,比如结账流程通常就是可以被别的流程调用的。可以类比成函数调用另外一个工具函数。目的是实现流程的模块化(Modularization)和重用(reuse)。

当流程执行到达Call Activity时,将创建一个新的子流程的流程实例,该实例用于执行子流程,这样就实现了原来主流程实例和新创建的子流程实例并行执行。主流程实例会等着子流程完全结束,然后再继续执行原来流程的后续环节。

在BPMN 2.0中Call Activity的符号是一个加粗的圆角矩形:

image-20210119144909417

这个符号很容易跟那个折叠的嵌入式子流程弄混,区别是Call Activity的边框要更厚一些:

image-20210119151445386

从BPMN XML的角度来看,callActivity元素通过一个必填的calledElement属性,去引用需要调用的流程定义。也就是Camunda Modeler中的流程id:

image-20210119152111746

注意:子流程的流程定义是在运行时才进行解析的。 这意味着如果有需要的话,子流程可以独立于主流程单独进行部署。而不用纠结谁先谁后。

调用绑定

Call Activity通过指定它的calledElement属性为流程定义名称,这样就可以引用到要调用的子流程,当只设置这个calledElement属性时,默认会调用引用到的那个流程的最新版本流程定义。

1
2
3
<bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="HR" camunda:calledElementTenantId="demo">
...
</bpmn:callActivity>

为了调用其它版本的子流程,可以通过定义指定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
2
3
 <bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="HR" camunda:calledElementBinding="versionTag" camunda:calledElementVersionTag="xxx" camunda:calledElementTenantId="demo">
... ...
</bpmn:callActivity>

如果在设计时不知道租户ID,则也可以使用表达式:

1
2
3
<callActivity id="callSubProcess" calledElement="checkCreditProcess"
camunda:calledElementTenantId="${ myBean.calculateTenantId(variable) }">
</callActivity>

表达式(expression)也允许使用父流程实例的租户ID,而不是使用父流程定义的租户ID:

1
2
3
<callActivity id="callSubProcess" calledElement="checkCreditProcess"
camunda:calledElementTenantId="${ execution.tenantId }">
</callActivity>

传递变量

可以将父流程的流程变量传递给子流程,反之亦然。这些变量数据在启动时被复制到子流程中,然后在子流程结束时再复制回主流程。

父流程传递给子流程

选中Call Activity组件之后,在属性面板中切换到Variables标签,点击In Mapping区域右上角的小加号,添加一条记录。

  • Type选择Source
  • Source输入框中输入父流程的一个变量名称,比如:ageInMainProcess
  • Target输入框中输入当父流程的变量被复制到子流程后,子流程要用哪个名字的变量来承接,比如:ageInSubProcess

image-20210118152132225

BPMN XML:

1
2
3
4
5
6
7
<bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="HR" camunda:calledElementTenantId="demo">
<bpmn:extensionElements>
<camunda:in source="nameInMainProcess" target="nameInSubProcess" />
<camunda:in source="ageInMainProcess" target="ageInSubProcess" />
</bpmn:extensionElements>
... ...
</bpmn:callActivity>

启动父流程:

image-20210118154025646

父流程变量:

image-20210118154512187

子流程变量:

image-20210118154533499

更近一步,也可以配置将所有流程变量都传递到子流程,这样就不用逐个设置需要传递的变量名称了。选中Call Activity组件之后,在属性面板中切换到Variables标签,点击In Mapping区域右上角的小加号,添加一条记录。记录中Type选择all

image-20210118174940675

BPMN XML:

1
2
3
4
5
6
<bpmn:callActivity id="Activity_1vme4hv" name="IT环节" calledElement="StaffEnrollment_IT" camunda:calledElementTenantId="demo">
<bpmn:extensionElements>
<camunda:in variables="all" />
</bpmn:extensionElements>
... ...
</bpmn:callActivity>

子流程传递给父流程

选中Call Activity组件之后,在属性面板中切换到Variables标签,点击Out Mapping区域右上角的小加号,添加一条记录。

  • Type选择Source
  • Source输入框中输入子流程的一个变量名称,比如:bomInSubProcess
  • Target输入框中输入当子流程的变量被复制回父流程后,要用哪个名字的变量来承接,比如:bomInSuperProcess

image-20210118161524693

BPMN XML:

1
2
3
4
5
6
<bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="StaffEnrollment_HR" camunda:calledElementTenantId="demo">
<bpmn:extensionElements>
<camunda:out source="bomInSubProcess" target="bomInSuperProcess" />
</bpmn:extensionElements>
... ...
</bpmn:callActivity>

启动父流程:

image-20210118154025646

父流程变量:

image-20210118162654442

子流程变量:

image-20210118162707074

为子流程添加一个变量(这个变量值会复制回主流程):

image-20210118162833145

子流程结束后,查看父流程的变量:

image-20210118164241060

更近一步,也可以配置将所有流程变量都传递回给父流程,这样就不用逐个设置需要传递的变量名称了。选中Call Activity组件之后,在属性面板中切换到Variables标签,点击Out Mapping区域右上角的小加号,添加一条记录。记录中Type选择all

image-20210118175920784

BPMN XML:

1
2
3
4
5
6
<bpmn:callActivity id="Activity_1vme4hv" name="IT环节" calledElement="StaffEnrollment_IT" camunda:calledElementTenantId="demo">
<bpmn:extensionElements>
<camunda:out variables="all" />
</bpmn:extensionElements>
... ...
</bpmn:callActivity>

表达式

也可以在输入/输出变量这里,使用表达式来进行设置:

1
2
3
4
5
6
<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
<extensionElements>
<camunda:in sourceExpression="${x+5}" target="y" />
<camunda:out sourceExpression="${y+5}" target="z" />
</extensionElements>
</callActivity>

因此,最终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

image-20210118183644298

再将属性面板的Tab中切换到Variables,点击In Mapping区域右上角的小加号,添加一条记录。记录中Type选择all,并勾选最下面的Local复选框:

image-20210118185037725

BPMN XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bpmn:callActivity id="Activity_0j1gipz" name="部门助理环节" calledElement="StaffEnrollment_DeptAssistant" camunda:calledElementTenantId="demo">
<bpmn:extensionElements>
<!-- 输入的参数 -->
<camunda:inputOutput>
<camunda:inputParameter name="var1">
<camunda:script scriptFormat="groovy">sum = a + b + c</camunda:script>
</camunda:inputParameter>

<camunda:inputParameter name="var2" />
</camunda:inputOutput>

<!-- 映射给子流程的变量:设置local=true,表示只将Local类型的变量传递给子流程 -->
<camunda:in local="true" variables="all" />
</bpmn:extensionElements>
... ...
</bpmn:callActivity>

最终子流程可以看到的变量:

Name Type Value Scope
var1 Integer 6 员工入职_部门助理环节
var2 String 员工入职_部门助理环节

一样的道理,对于输出参数也可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<callActivity id="callSubProcess" calledElement="checkCreditProcess" >
<extensionElements>
<!-- Input/Output parameters -->
<camunda:inputOutput>
<camunda:outputParameter name="var1">
<camunda:script scriptFormat="groovy">sum = a + b + c</camunda:script>
</camunda:outputParameter>
<camunda:outputParameter name="var2"></camunda:outputParameter>
</camunda:inputOutput>

<!-- Mapping from called instance -->
<camunda:out variables="all" local="true" />
</extensionElements>
</callActivity>

当子流程实例结束之后,由于将camunda:out元素的local属性设置为了true,正在执行的子流程中的所有变量都会被映射到Local变量中。

这些变量可以使用Output Parameter Mapping功能再映射到流程实例中。Call Activity结束后,所有未经camunda:outputParameter元素声明的变量都不再可用。

变量映射委托

输入/输出变量的映射也可以被委托(官方文档中所说的委托,我理解为就是把一部分要实现或者需要自定义的功能,交给用户自己的代码来实现,交给别人来实现也就是所谓的“委托”)。这意味着可以通过Java代码来实现输入/输出变量。为了实现这样的功能,必须实现DelegateVariableMapping接口。

1
2
3
4
5
6
7
8
9
10
11
12
public class DelegatedVarMapping implements DelegateVariableMapping {

@Override
public void mapInputVariables(DelegateExecution execution, VariableMap variables) {
variables.putValue("inputVar", "inValue");
}

@Override
public void mapOutputVariables(DelegateExecution execution, VariableScope subInstance) {
execution.setVariable("outputVar", "outValue");
}
}

我们可以通过如下2种委托方案实现变量映射:

引用方式(Reference)

设置Camunda的扩展属性variableMappingClass,指定这个属性的值为一个实现了DelegateVariableMapping接口的类的全限定类名。

首先选中Call Activity,切换到General标签,在Delegate Variable Mapping下拉菜单中选择variableMappingClass,然后在下面的Class输入框中填写全限定类名称:

image-20210119101153317

BPMN XML:

1
2
3
<bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="StaffEnrollment_HR" camunda:calledElementTenantId="demo" camunda:variableMappingClass="me.ningyu.itsm.DelegatedVarMapping">
... ...
</bpmn:callActivity>

表达式方式(Expression)

设置Camunda的扩展属性variableMappingDelegateExpression,指定这个属性的值为一个表达式。这里允许指定一个表达式,该表达式可以解析为实现DelegateVariableMapping接口的对象。

首先选中Call Activity,切换到General标签,在Delegate Variable Mapping下拉菜单中选variableMappingDelegateExpression,然后在下面的Delegate Expression输入框中填入表达式语句:

image-20210119102522087

BPMN XML:

1
2
3
<bpmn:callActivity id="Activity_1vykv71" name="HR环节" calledElement="StaffEnrollment_HR" camunda:calledElementTenantId="demo" camunda:variableMappingDelegateExpression="${expr}">
... ...
</bpmn:callActivity>

传递BusinessKey

父流程可以传递BusinessKey给子流程。当子流程启动的时候BusinessKey被复制到子流程中去。

注意你无法将BusinessKey返回还给父流程,因为BusinessKey是不可更改的。

首先选中Call Activity,切换到General标签,勾选中Business Key,然后会在下方出现一个输入框,输入框中有默认值#{execution.processBusinessKey},表示传递父流程实例的BusinessKey作为子流程实例的BusinessKey,你可以直接使用这个默认值,或者手动修改输入框中的值。

image-20210119104358162

注意事项

  • SourceTarget输入框中输入的变量名,可以相同,也可以不同。
  • 如果勾选了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
2
3
4
curl --location --request PUT 'http://11.11.176.126:48086/engine-rest/task/c2f6d18f-596b-11eb-b228-0a58ac0009d7/localVariables/bomInSubProcess' \
--header 'Content-Type: application/json' \
--data-raw '{"value" : "入职手续清单", "type": "String"}
'

image-20210118171019391

  • 声明在inputOutput标签页中的参数,它们的作用域只在当前的活动/任务,我们可以这样验证:

    1. 选中Call Activity

    2. 先切换到Variables标签页,在In Mapping中添加一个TypeAll的映射类型,注意先不要勾选Local

    3. 再切换到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

    4. 然后在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
    5. 登陆到Camunda Cockpit,在流程实例的详情中可以查看到5个变量:

      Name Type Value Scope
      a Integer 1 员工入职
      b Integer 2 员工入职
      c Integer 3 员工入职
      var1 Integer 6 HR环节
      var2 String Test HR环节
    6. 从Camunda Cockpit的Called Process Instances标签进入到子流程详情中,可以发现上述的5个变量全都被映射到了子流程中。

    7. 回到Camunda Modeler中,切换到Variables标签页,在In Mapping中添加一个Type为All的映射类型,这次勾选Local,重新部署流程。

    8. 参照上面的方法再次发起一个流程实例,添加同样的变量,登陆到Camunda Cockpit,在子流程的流程实例的详情中只可以查看到2个变量:

      Name Type Value Scope
      var1 Integer 6 员工入职_HR环节
      var2 String Test 员工入职_HR环节

参考

EOF