camunda中流程变量如何使用
本文章介绍camunda流程引擎中变量的概念。流程变量可用于向流程运行时状态添加数据,或者更具体地说,向变量作用域添加数据。更改这些实体状态的各种 API 方法允许更新附加的变量。通常,变量由名称和值组成。该名称用于跨流程构造进行标识。例如,如果一个活动设置了一个名为 var 的变量,则后续活动可以使用此名称访问它。变量的值是一个 Java 对象。
1、流程变量作用域和可见性
所有可以具有变量的实体都称为变量作用域。这些是执行(包括流程实例)和任务。如概念部分所述,流程实例的运行时状态由执行树表示。比如以下流程模型,其中红点标记的活动任务:
此流程的运行时结构如下:
因为有网关节点,这个流程实例就是一个两个子执行(execution)的流程实例,每个子执行都创建了一个任务。所有这五个实体都是可变作用域,箭头标记父子关系。在父作用域上定义的变量可以在每个子作用域中访问,除非子作用域定义了同名的变量。反之,子变量不能从父作用域访问。直接附加到相关作用域的变量称为局部变量。请考虑以下变量分配给作用域:
在这种情况下,在处理任务 1 时,变量 worker 和 customer 是可访问的。请注意,由于作用域的结构,变量 worker 可以定义两次,以便任务1访问与任务2不同的工作线程变量。但是,两者都共享变量 customer,这意味着如果其中一个任务更新了该变量,则此更改对另一个任务也可见。
这两个任务都可以访问两个变量,而这些变量都不是局部变量。所有三个执行(execution)都有一个局部变量。
现在,假设我们在任务1上设置了一个局部变量 customer:
虽然仍然可以从任务 1 访问名为 customer 和 worker 的两个变量,但执行 1 上的 customer 变量是隐藏的,因此可访问的客户变量是任务 1 的局部变量。
通常,变量在以下情况下是可访问的:
2、设置和检索流程变量
为了设置和检索变量,流程引擎提供了一个 Java API,它允许从 Java 对象中设置变量并以setting 的形式检索它们。在内部,引擎将变量保存到数据库,因此应用序列化。对于大多数应用来说,这是一个无关紧要的细节。但是,有时,在使用自定义 Java 类时,变量的序列化值是值得关注的。想象一下,一个管理许多流程应用程序的监控应用程序。它与这些应用程序的类分离,因此无法访问其 Java 表示中的自定义变量。对于这些情况,流程引擎提供了一种检索和操作序列化值的方法。这归结为两个 API:
- Java 对象值 API(Java Object Value API):变量表示为 Java 对象。这些对象可以直接设置为值并以相同的形式检索。这是更简单的 API,是将代码作为流程应用程序的一部分实现,推荐使用这种方法。
- 类型化值 API(Typed Value API:):变量值包装在用于设置和检索变量的所谓类型化值中。类型化值提供对元数据的访问,例如引擎序列化变量的方式,以及变量的序列化表示形式(具体取决于类型)。元数据还包含变量是否为瞬态的信息。
例如,以下代码使用这两个 API 检索和设置两个整数变量:
// Java Object API: Get Variable
Integer val1 = (Integer) execution.getVariable("val1");
// Typed Value API: Get Variable
IntegerValue typedVal2 = execution.getVariableTyped("val2");
Integer val2 = typedVal2.getValue();
Integer diff = val1 - val2;
// Java Object API: Set Variable
execution.setVariable("diff", diff);
// Typed Value API: Set Variable
IntegerValue typedDiff = Variables.integerValue(diff);
execution.setVariable("diff", typedDiff);
2.1、将变量设置为特定范围
可以从脚本(scripts)、输入\输出映射(input\output mapping)、侦听器(listeners )和服务任务(service tasks)中将变量设置到特定范围内。此功能的实现使用活动 ID 来标识目标范围,如果找不到用于设置变量的范围,则会引发异常。此外,一旦找到目标作用域,将在其中本地设置变量,这意味着即使目标作用域没有具有给定 id 的变量,也不会执行到父作用域的传播。
以下是脚本 executionListener 的示例用法:
<camunda:executionListener event="end">
<camunda:script scriptFormat="groovy"><![CDATA[execution.setVariable("aVariable", "aValue","aSubProcess");]]></camunda:script>
</camunda:executionListener>
另一个用法示例是输入\输出映射,使用DelegateVariableMapping实现
public class SetVariableToScopeMappingDelegate implements DelegateVariableMapping {
@Override
public void mapInputVariables(DelegateExecution superExecution, VariableMap subVariables) {
}
@Override
public void mapOutputVariables(DelegateExecution superExecution, VariableScope subInstance) {
superExecution.setVariable("aVariable","aValue","aSubProcess");
}
}
here 变量将在“aSubProcess”中本地设置,即使变量未事先在“aSubProcess”中本地设置,也不会传播到父范围。
3、支持的变量值
流程引擎支持以下变量值类型:
根据变量的实际值,分配不同的类型。在可用类型中,有九种原始值( primitive value)类型,这意味着它们存储简单标准 JDK 类的值,而无需其他元数据:
- boolean:实例java.lang.Boolean
- bytes:实例byte[]
- short:实例java.lang.Short
- integer:实例java.lang.Integer
- long:实例java.lang.Long
- double:实例java.lang.Double
- date:实例java.util.Date
- string:实例java.lang.String
- null:引用null
原始值( primitive value)与其他变量值的不同之处在于,它们可以在 API 查询(如流程实例查询)中用作筛选条件。
file类型可用于存储文件或输入流的内容以及元数据,例如文件名、编码和文件内容对应的 MIME 类型。
object值类型表示定制 Java 对象。当此类变量被保留时,其值将根据序列化流程进行序列化。这些流程是可配置和可交换的。
3.1、字符串长度限制
string值存储在数据库的列(n)varchar类型中,长度限制为 4000(Oracle 为 2000)。根据正在使用的数据库和配置的字符集,此长度限制可能会导致不同数量的实数字符。变量值长度不会在 Camunda 引擎中进行验证,但值会“按原样”发送到数据库,如果超过长度限制,将引发数据库级异常。如果需要验证,可以单独实现,并且必须在调用用于设置变量的 Camunda API 之前进行。
流程变量可以存储为 Camunda Spin 插件提供的 JSON 和 XML 等格式。Spin 为object类型的变量提供序列化程序,以便 Java 变量可以以这些格式保存到数据库中。此外,可以通过值类型XML和JSON将JSON和XML文档直接存储为Spin对象。与普通字符串变量相反,Spin对象提供了一个流畅的API来对此类文档执行常见操作,如读取和写入属性。
3.2、对象值序列化
当将对象值传递给流程引擎时,可以指定序列化格式来告诉流程引擎以特定格式存储该值。基于这种格式,引擎查找一个序列化程序。序列化程序能够将Java对象序列化为指定的格式,并从该格式的表示中将其反序列化。这意味着,对于不同的格式可能有不同的序列化程序,并且可以实现自定义序列化程序以便以特定格式存储自定义对象。
流程引擎为formatapplication/x-java-serialized-object提供了一个内置的对象序列化程序。它能够序列化实现接口Java.io.Serializable的Java对象,并应用标准Java对象序列化。
使用类型值API设置变量时,可以指定所需的序列化格式:
CustomerData customerData = new CustomerData();
ObjectValue customerDataValue = Variables.objectValue(customerData)
.serializationDataFormat(Variables.SerializationDataFormats.JAVA)
.create();
execution.setVariable("someVariable", customerDataValue);
除此之外,流程引擎配置还有一个选项defaultSerializationFormat,当没有请求特定格式时使用该选项。此选项默认为application/x-java-serialized-object。
4、Java 对象 API
使用 Java 中的流程变量最方便的方法是使用它们的 Java 对象表示。只要流程引擎提供变量访问,就可以在此表示形式中访问流程变量,因为对于自定义对象,引擎知道所涉及的类。例如,以下代码设置并检索给定流程实例的变量:
com.example.Order order = new com.example.Order();
runtimeService.setVariable(execution.getId(), "order", order);
com.example.Order retrievedOrder = (com.example.Order) runtimeService.getVariable(execution.getId(), "order");
此代码将变量设置在变量作用域层次结构中可能的最高点。这意味着,如果变量已经存在(无论是在此次执行中还是在其任何父作用域中),它都会被更新。如果变量还不存在,则在最高范围(即流程实例)中创建该变量。如果一个变量应该在提供的执行中准确设置,那么可以使用本地方法。例如
com.example.Order order = new com.example.Order();
runtimeService.setVariableLocal(execution.getId(), "order", order);
com.example.Order retrievedOrder = (com.example.Order) runtimeService.getVariable(execution.getId(), "order");
com.example.Order retrievedOrder = (com.example.Order) runtimeService.getVariableLocal(execution.getId(), "order");
// both methods return the variable
每当在其 Java 表示中设置变量时,流程引擎都会自动确定合适的值序列化程序,或者如果提供的值无法序列化,则引发异常。
5、类型化值 API
在访问变量的序列化表示很重要,或者必须提示引擎以某种格式序列化值的情况下,可以使用基于类型值的API。与基于Java-Object的API相比,它将变量值封装在所谓的TypedValue中。这样的类型化值允许更丰富地表示变量值。
为了方便地构造类型化的值,Camunda平台提供了org.Camunda.bpm.engine.variable类。变量。此类包含静态方法,这些方法允许创建单一类型的值以及以流畅的方式创建类型值的映射。
6、流程变量值类型
6.1、基元值类型(Primitive Values)
以下代码通过将单个String变量指定为类型化值来设置该变量:
StringValue typedStringValue = Variables.stringValue("a string value");
runtimeService.setVariable(execution.getId(), "stringVariable", typedStringValue);
StringValue retrievedTypedStringValue = runtimeService.getVariableTyped(execution.getId(), "stringVariable");
String stringValue = retrievedTypedStringValue.getValue(); // equals "a string value"
请注意,使用此 API,变量值周围还有一个抽象级别。因此,为了获得真实值,有必要解开实际值。
6.2、文件值类型(File Values)
对于普通的String值,基于Java-Object的API更加简洁。因此,让我们考虑更丰富的数据结构的价值。
文件可以作为BLOB持久存在数据库中。文件值类型允许存储其他元数据,如文件名和mime类型。以下示例代码从文本文件创建文件值:
FileValue typedFileValue = Variables
.fileValue("addresses.txt")
.file(new File("path/to/the/file.txt"))
.mimeType("text/plain")
.encoding("UTF-8")
.create();
runtimeService.setVariable(execution.getId(), "fileVariable", typedFileValue);
FileValue retrievedTypedFileValue = runtimeService.getVariableTyped(execution.getId(), "fileVariable");
InputStream fileContent = retrievedTypedFileValue.getValue(); // a byte stream of the file contents
String fileName = retrievedTypedFileValue.getFilename(); // equals "addresses.txt"
String mimeType = retrievedTypedFileValue.getMimeType(); // equals "text/plain"
String encoding = retrievedTypedFileValue.getEncoding(); // equals "UTF-8"
更改文件File值.要更改或更新文件值,您必须创建一个具有相同名称和新内容的新FileValue,因为所有类型化的值都是不可变的:
InputStream newContent = new FileInputStream("path/to/the/new/file.txt");
FileValue fileVariable = execution.getVariableTyped("addresses.txt");
Variables.fileValue(fileVariable.getName()).file(newContent).encoding(fileVariable.getEncoding()).mimeType(fileVariable.getMimeType()).create();
6.3、对象值
自定义Java对象可以使用值类型对象进行序列化。使用类型化值API的示例:
com.example.Order order = new com.example.Order();
ObjectValue typedObjectValue = Variables.objectValue(order).create();
runtimeService.setVariableLocal(execution.getId(), "order", typedObjectValue);
ObjectValue retrievedTypedObjectValue = runtimeService.getVariableTyped(execution.getId(), "order");
com.example.Order retrievedOrder = (com.example.Order) retrievedTypedObjectValue.getValue();
这又等同于基于 Java 对象的 API。但是,现在可以告诉引擎在保留值时要使用哪种序列化格式。例如:
ObjectValue typedObjectValue = Variables
.objectValue(order)
.serializationDataFormat(Variables.SerializationDataFormats.JAVA)
.create();
创建一个值,该值由引擎的内置Java对象序列化程序序列化。此外,检索到的ObjectValue实例还提供了其他变量详细信息:
// returns true
boolean isDeserialized = retrievedTypedObjectValue.isDeserialized();
// returns the format used by the engine to serialize the value into the database
String serializationDataFormat = retrievedTypedObjectValue.getSerializationDateFormat();
// returns the serialized representation of the variable; the actual value depends on the serialization format used
String serializedValue = retrievedTypedObjectValue.getValueSerialized();
// returns the class com.example.Order
Class<com.example.Order> valueClass = retrievedTypedObjectValue.getObjectType();
// returns the String "com.example.Order"
String valueClassName = retrievedTypedObjectValue.getObjectTypeName();
当调用应用程序不拥有实际变量值的类(即com.example.Order未知)时,序列化详细信息非常有用。在这些情况下,runtimeService.getVariableTyped(execution.getId(),“order”)将引发异常,因为它会立即尝试反序列化变量值。在这种情况下,可以使用调用runtimeService.getVariableTyped(execution.getId(),“order”,false)。附加布尔参数告诉流程引擎不要尝试反序列化。在这种情况下,调用isDeserialized()将返回false,而像getValue()和getObjectType()这样的调用将引发异常。尽管如此,调用getValueSerialized()和getObjectTypeName()是访问变量的一种方式。
同样,也可以从变量的序列化表示中设置变量:
String serializedOrder = "...";
ObjectValue serializedValue =
Variables
.serializedObjectValue(serializedOrder)
.serializationDataFormat(Variables.SerializationDataFormats.JAVA)
.objectTypeName("com.example.Order")
.create();
runtimeService.setVariableLocal(execution.getId(), "order", serializedValue);
ObjectValue retrievedTypedObjectValue = runtimeService.getVariableTyped(execution.getId(), "order");
com.example.Order retrievedOrder = (com.example.Order) retrievedTypedObjectValue.getValue();
不一致的变量状态
设置序列化变量值时,不检查序列化值的结构是否与变量值应为其实例的类兼容。在设置上述示例中的变量时,所提供的序列化值不会根据com.example的结构进行验证。顺序因此,只有在调用runtimeService#getVariableTyped时才会检测到无效的变量值。
Java 序列化格式
在使用变量的序列化表示时,默认情况下禁止使用Java序列化格式。您应该使用另一种格式(JSON或XML),或者在javaSerializationFormatEnabled配置标志的帮助下显式启用Java序列化。
6.4、JSON 和 XML 值
Camunda Spin插件为JSON和XML文档提供了一个抽象,有助于它们的处理和操作。这通常比将此类文档存储为纯字符串变量更方便。有关存储JSON文档和存储XML文档的详细信息,请参阅Camunda SPIN上的文档。
6.5、瞬态变量
只有通过基于类型值的API(typed-value-based API)才能声明瞬态变量。它们不会保存到数据库中,并且仅在当前事务期间存在。流程实例执行过程中的每个等待状态都会导致所有瞬态变量的丢失。这通常发生在外部服务当前不可用、用户任务已到达或进程执行正在等待消息、信号或条件时。请小心使用此功能。
任何类型的变量都可以使用Variables类声明为transient,并将参数isTransient设置为true。
// primitive values
TypedValue typedTransientStringValue = Variables.stringValue("foobar", true);
// object value
com.example.Order order = new com.example.Order();
TypedValue typedTransientObjectValue = Variables.objectValue(order, true).create();
// file value
TypedValue typedTransientFileValue = Variables.fileValue("file.txt", true)
.file(new File("path/to/the/file.txt"))
.mimeType("text/plain")
.encoding("UTF-8")
.create();
瞬态变量可以通过 REST API 使用,例如在启动新的流程实例时:https://docs.camunda.org/rest/camunda-bpm-platform/7.19/#tag/Process-Definition/operation/startProcessInstance
6.6、设置多个类型化值
类似于基于Java-Object的API,也可以在一个API调用中设置多个类型化的值。Variables类提供了一个流畅的API来构造类型化值的映射:
com.example.Order order = new com.example.Order();
VariableMap variables =
Variables.createVariables()
.putValueTyped("order", Variables.objectValue(order).create())
.putValueTyped("string", Variables.stringValue("a string value"))
.putValueTyped("stringTransient", Variables.stringValue("foobar", true));
runtimeService.setVariablesLocal(execution.getId(), "order", variables);
7、API 的可互换性
两个API在相同的实体上提供不同的视图,因此可以根据需要进行组合。例如,使用基于Java-Object的API设置的变量可以作为类型化值检索,反之亦然。由于类VariableMap实现了Map接口,所以也可以将普通Java对象和类型化的值放入该映射中。
您应该使用哪种API?最适合你的目的。当您确信您始终可以访问所涉及的值类时,例如在流程应用程序(如JavaDelegate)中实现代码时,则基于Java-Object的API更易于使用。当您需要访问值特定的元数据(如序列化格式)或将变量定义为瞬态时,那么基于类型值的API就是您的选择。
8、输入/输出变量映射
为了提高源代码和业务逻辑的可重用性,Camunda平台提供了流程变量的输入/输出映射。这可以用于任务、事件和子流程。
为了使用变量映射,必须将Camunda扩展元素inputOutput添加到元素中。它可以包含多个inputParameter和outputParameter元素,用于指定应映射的变量。inputParameter的name属性表示活动内部的变量名(要创建的局部变量),而outputParameter的name属性表示活动外部的变量名。
输入/输出参数的内容指定映射到相应变量的值。它可以是一个简单的常量字符串或表达式。一个空体将变量设置为null值。
<camunda:inputOutput>
<camunda:inputParameter name="x">foo</camunda:inputParameter>
<camunda:inputParameter name="willBeNull"/>
<camunda:outputParameter name="y">${x}</camunda:outputParameter>
<camunda:outputParameter name="z">${willBeNull == null}</camunda:outputParameter>
</camunda:inputOutput>
甚至可以使用List和Map等复杂结构。两者也可以嵌套。
<camunda:inputOutput>
<camunda:inputParameter name="x">
<camunda:list>
<camunda:value>a</camunda:value>
<camunda:value>${1 + 1}</camunda:value>
<camunda:list>
<camunda:value>1</camunda:value>
<camunda:value>2</camunda:value>
<camunda:value>3</camunda:value>
</camunda:list>
</camunda:list>
</camunda:inputParameter>
<camunda:outputParameter name="y">
<camunda:map>
<camunda:entry key="foo">bar</camunda:entry>
<camunda:entry key="map">
<camunda:map>
<camunda:entry key="hello">world</camunda:entry>
<camunda:entry key="camunda">bpm</camunda:entry>
</camunda:map>
</camunda:entry>
</camunda:map>
</camunda:outputParameter>
</camunda:inputOutput>
脚本也可以用于提供变量值。有关如何指定脚本,请参阅脚本一章中相应的部分。
输入/输出映射好处的一个简单例子是复杂的计算,它应该是多个过程定义的一部分。此计算可以作为独立的委托代码或脚本开发,并在每个进程中重复使用,即使进程使用不同的变量集也是如此。输入映射用于将不同的过程变量映射到复杂计算活动所需的输入参数。因此,输出映射允许在进一步的过程执行中利用计算结果。
更详细地说,让我们假设这样的计算是由Java Delegate类org.camunda.bpm.example实现的。ComplexCalculation。此委托需要一个userId和一个costSum变量作为输入参数。然后,它计算三个值,悲观预测、现实预测和乐观预测,这三个值是对客户面临的未来成本的不同预测。在第一个过程中,两个输入变量都可用作过程变量,但名称不同(id、sum)。从这三个结果来看,流程仅使用realisticForecast,它在后续活动中依赖于名称预测。相应的输入/输出映射如下所示:
<serviceTask camunda:class="org.camunda.bpm.example.ComplexCalculation">
<extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="userId">${id}</camunda:inputParameter>
<camunda:inputParameter name="costSum">${sum}</camunda:inputParameter>
<camunda:outputParameter name="forecast">${realisticForecast}</camunda:outputParameter>
</camunda:inputOutput>
</extensionElements>
</serviceTask>
在第二个过程中,让我们假设costSum变量必须根据三个不同映射的属性进行计算。此外,该过程还取决于变量avgForecast作为三个预测的平均值。在这种情况下,映射如下所示:
<serviceTask camunda:class="org.camunda.bpm.example.ComplexCalculation">
<extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="userId">${id}</camunda:inputParameter>
<camunda:inputParameter name="costSum">
${mapA[costs] + mapB[costs] + mapC[costs]}
</camunda:inputParameter>
<camunda:outputParameter name="avgForecast">
${(pessimisticForecast + realisticForecast + optimisticForecast) / 3}
</camunda:outputParameter>
</camunda:inputOutput>
</extensionElements>
</serviceTask>
Camunda工作流体验环境:http://www.yunchengxc.com
Camunda7.19官方文档:https://docs.camunda.org/manual/7.19/user-guide/process-engine/variables/