Flowable 中文文档

Flowable 中文文档

  • 指南
  • Java文档
  • 博客
  • Flowable UI
  • 加入交流群
  • 英文文档

›所有文章

所有文章

  • Flowable 后端脚本
  • ​​AI赋能Flowable:破解传统BPM的响应延迟与决策盲区​​
  • Flowable 后端表达式
  • Flowable中的四种核心流程控制模式:会签、或签、分支与并行
  • Flowable 案例迁移
  • Flowable 自动部署模型
  • Flowable-UI 入门指南:从零开始的 BPM 之旅
  • Flowable 数据库表结构详解
  • Spring Boot + Flowable 工作流开发教程:整合 BPMN 和 CMMN 实战
  • BPMN、CMMN和DMN:工作流引擎三剑客的深度对比
  • 工作流引擎介绍与选型指南
  • 欢迎来到 Flowable 中文博客

Flowable 后端脚本

May 6, 2025

Flowable 脚本 API 架构

简介

脚本可以在各种场景中使用,如流程的脚本任务、案例的脚本任务以及使用脚本的服务注册定义中。

可以使用符合JSR-223标准的脚本语言(如JavaScript或Groovy)来编写脚本,Flowable将脚本API暴露到脚本上下文中,您可以访问各种实用工具,以及创建JSON对象、访问服务、请求输入变量和注册输出参数,当然这取决于您的具体使用场景。

本文档主要介绍Flowable脚本API,它通过flw对象进行访问和使用。

输入和输出

在脚本中,您可以使用flw.getInput(name)获取输入值,根据上下文的不同,返回的可能是具有请求名称的变量或具有该名称的输入参数。

同样,使用flw.setOutput(name, value)可以设置变量值或注册输出参数值。

以下方法可用:

  • flw.getInput(name) - 根据上下文的不同,返回请求名称的变量或对应名称的输入参数。
  • flw.setOutput(name, value) - 设置变量值或注册输出参数值。
  • flw.setTransientOutput(name, value) - 设置临时变量值。
  • flw.setLocalOutput(name, value) - 将变量值设置为本地作用域。

脚本任务示例:

  • flw.getInput('foo'); 将返回名为foo的变量。
  • flw.setOutput('bar', myBarValue); 将存储一个名为bar的新变量,其值由本地脚本变量myBarValue提供。

使用脚本的注册服务示例:

  • flw.getInput('foo'); 将返回名为foo的映射输入参数。
  • flw.setOutput('bar', myBarValue); 将本地脚本变量myBarValue提供的值注册为名为bar的输出参数。

BPMN错误

脚本API支持使用以下方法抛出BPMN错误:

  • flw.bpmn.throwError('ERROR_CODE')
  • flw.bpmn.throwError('ERROR_CODE', 'Error Message')

BPMN错误可以在BPMN模型中使用错误边界事件或错误事件子流程进行处理,这使其成为基于模型的错误处理的强大工具,无需修改Java代码。

这些工具方法相当于便捷地执行throw new org.flowable.engine.delegate.BpmnError('ERROR_CODE')。

JSON工具

脚本API还支持各种围绕JSON对象处理的工具,如创建JSON对象或数组,或将JSON字符串转换为JSON对象结构。

您可以通过flw.json访问这些工具。

要创建一个新的空JSON对象,使用flw.json.createObject(),并将结果存储为本地变量,或使用返回的JSON对象的流式API来创建其值。

带有简单值的JSON对象

只使用值而不带嵌套或列表值的简单JSON对象可以如下创建:

示例:

var myJson = flw.json.createObject();
myJson.putString('name', 'Sample Json object');
myJson.putInteger('sum', 100);
myJson.putBoolean('verified', true);

这个例子也可以写成:

var myJson = flw.json.createObject()
    .putString('name', 'Sample Json object')
    .putInteger('sum', 100)
    .putBoolean('verified', true);

或者,如果用作输出值:

flw.setOutput('myJsonOutputParameter', flw.json.createObject()
    .putString('name', 'Sample Json object')
    .putInteger('sum', 100)
    .putBoolean('verified', true));

脚本变量、JSON和FLOWABLE变量

有不同的方式来表示脚本中的数据:

  1. Groovy/JavaScript变量(以下为简化起见,专注于JavaScript)
  2. JSON
  3. Flowable变量

在JavaScript中,您可以创建对象结构并向这些对象添加不同的元素,包括例如日期。例如:{name: 'Sample Json object', myDate: new Date()}

将其转换为JSON将产生如下结果:{"test":"2024-01-17T05:53:01.880Z"}。这可以在JavaScript中分配给变量,但并不是每个JavaScript变量都是有效的JSON。例如,{test: () => { return 'test' }}在JavaScript中是有效的对象,但将其转换为JSON会导致一个空对象{}。

Flowable的flw.json API在不同脚本语言之间与向后兼容性无关。JavaScript执行来自Nashorn引擎,可能在不同的Java版本和/或类路径上可用的Nashorn引擎版本之间有所不同。

也可以直接使用JavaScript/Groovy创建JSON结构,只需注意这取决于执行引擎。

以下示例将使用JavaScript引擎创建一个对象,将其转换为JSON,然后将其添加为Flowable JSON变量testJson:

flw.setOutput("testJson",
   flw.json.stringToJson(
      JSON.stringify({name: 'Sample Json object', test: new Date()})
   )
);

嵌套JSON对象

嵌套对象也可以轻松创建:

var myJson = flw.json.createObject()
    .putString('name', 'Sample Json object');

myJson.createChildObject('nestedObject')
    .putString('nestedObjectName', 'Nested Json object');

注意

createChildObject方法将创建一个新的子JSON对象,并使用提供的字段名将其添加到当前对象(从中调用该方法的对象)。

请注意返回值;createObject()将返回根JSON对象,但createChildObject始终只返回新创建的子对象以进行进一步处理,如添加值。如果您最终需要使用根对象,请确保保留一个包含返回的根JSON对象的变量,而不仅仅是最新的嵌套对象。

JSON数组

JSON数组可以作为"根"JSON对象创建,也可以作为嵌套对象创建。

创建JSON数组可以使用json工具的createArray方法:

var myJsonArray = flw.json.createArray()
    .addString('hello')
    .addString('world');

创建嵌套JSON数组也很简单:

var myJson = flw.json.createObject()
    .putString('name', 'Sample Json object');

myJson.createChildArray('nestedList')
    .addString('hello')
    .addString('world');

注意

要向数组添加值,请使用addXY方法,而不是用于设置JSON对象值的putXY方法。

可用方法

以下部分列出了使用flw.json.createObject,flw.json.createArray或flw.json.stringToJson创建的JSON对象的可用方法。

通用方法

这是可用通用方法的列表:

  • size() 此对象的属性数量或数组长度。
  • isArray() 当节点是数组节点时返回true。
  • isObject() 当节点是对象节点时返回true。
  • isValue() 当节点是值节点时返回true。
  • isPresent() 当节点存在时返回true。
  • isNonNull() 当节点存在且不为null时返回true。
  • iterator() 返回一个迭代器以迭代数组元素。对于对象节点,返回带有对象本身的单个元素迭代器。

对象方法

这是JSON对象(非数组)可用方法的列表:

  • putString(fieldName, value) 带有String值。
  • putInteger(fieldName, value) 带有Number值。
  • putInt(fieldName, value) 带有int值。
  • putBoolean(fieldName, value) 带有boolean值。
  • putShort(fieldName, value) 带有Number或short值。
  • putLong(fieldName, value) 带有Number或long值。
  • putDouble(fieldName, value) 带有Number或double值。
  • putFloat(fieldName, value) 带有Number或float值。
  • putObject(fieldName, value) 添加一个对象。该对象可以是null(则使用putNull)或另一个JSON对象或数组。
  • putNull(fieldName) 向对象添加null值。
  • createChildObject(fieldName) 创建一个新的子JSON对象并将其作为嵌套对象添加到当前对象。
  • createChildArray(fieldName) 创建一个新的JSON数组并将其作为子数组添加到当前对象。
  • path(path) 返回给定路径的JSON节点。当路径不存在时,结果对象对isPresent()返回false。
  • fieldNames() 返回对象节点的所有字段名称的集合。如果节点不是对象,则返回空集合。

数组方法

这是JSON数组(非对象)可用方法的列表:

  • addString(value) 带有String值。
  • addInteger(value) 带有Number值。
  • addInt(value) 带有int值。
  • addBoolean(value) 带有boolean值。
  • addShort(value) 带有Number或short值。
  • addLong(value) 带有Number或long值。
  • addDouble(value) 带有Number或double值。
  • addFloat(value) 带有Number或float值。
  • addNull() 向数组添加null值。
  • addObject(value) 向数组添加对象。该对象可以是null或另一个JSON对象或数组。
  • path(index) 将返回提供的索引处的元素。当索引不存在时,结果对象对isPresent()返回false。

值方法

这是值可用方法的列表。所有方法在被调用在非值或null节点上时都会抛出FlowableException。

  • asString() 将给定值节点作为字符串返回。将非字符串值转换为其字符串表示形式。
  • asInteger() 将数值作为整数返回。如果不是整数或整数溢出,则抛出FlowableException。
  • asLong() 将数值作为long返回。如果不是整数,则抛出FlowableException。
  • asDouble() 将数值作为double返回。
  • asBoolean() 返回布尔值。不将其他值类型强制转换为布尔值。
  • isString() 当节点是字符串时返回true。
  • isNumber() 当节点是数字时返回true。
  • isBoolean() 当节点是布尔值时返回true。

JSON字符串转换

如果您有表示JSON对象的字符串,可以将其转换为JSON对象以进行进一步处理,如果需要,也可以转回字符串。

  • flw.json.stringToJson
  • flw.json.jsonToString

示例:

var myJsonString = '{ "foo": "fooValue", "bar": "barValue", "sum": 100, "verified": true }';
var myJson = flw.json.stringToJson(myJsonString);
myJson.putString("my_value", "value");
var string = flw.json.jsonToString(myJson);

时间工具

脚本API还支持日期和时间处理的实用工具,可通过flw.time访问。 如果没有特别说明,默认时区始终是UTC。

您可以使用与表达式中相同的功能,使用flwTimeUtils。

以下是可用函数的列表:

函数描述
Instant now()返回UTC中的当前日期和时间。
Instant currentDate()返回UTC时间上午00:00的当前日期作为Instant。
LocalDate currentLocalDate()返回系统时区(即UTC)中的当前日期,作为LocalDate实例。
LocalDateTime currentLocalDateTime()返回系统时区(即UTC)中的当前日期和时间,作为LocalDateTime实例。
Instant instantFromTimestamp(long timestamp)从1970年1月1日以来经过的秒数(Unix时间戳)返回一个Instant。
Date dateFromTimestamp(long timestamp)从1970年1月1日以来经过的秒数(Unix时间戳)返回一个Date。
Instant parseInstant(Date date)从Date中返回一个Instant。
Instant parseInstant(String instantIsoString)将ISO8601格式的字符串解析为Instant。
Instant parseInstant(Object value, String pattern)使用提供的模式将对象解析为Instant。支持的输入是String、Json TextNode或null。
LocalDate parseLocalDate(Object value, String pattern)使用提供的模式将对象解析为LocalDate。支持的输入是String、Json TextNode或null。
LocalDateTime parseLocalDateTime(Object value, String pattern)使用提供的模式将对象解析为LocalDateTime。支持的输入是String、Json TextNode或null。
Instant asInstant(Object value)将值转换为带有UTC时区的Instant。支持的值有:Date、Instant(直接返回)、LocalDate、LocalDateTime、ISO8601格式的String或null。
Instant asInstant(Object value, String timeZoneId)将值转换为给定时区的Instant。支持的值有:Date、Instant(直接返回)、LocalDate、LocalDateTime、ISO8601格式的String或null。
LocalDate asLocalDate(Object value)将值转换为带有UTC时区的LocalDate。支持的值有:Date、Instant、LocalDate(直接返回)、LocalDateTime、ISO8601格式的String或null。
LocalDate asLocalDate(Object value, String timeZoneId)将值转换为给定时区的LocalDate。支持的值有:Date、Instant、LocalDate(直接返回)、LocalDateTime、ISO8601格式的String或null。
LocalDateTime asLocalDateTime(Object value)将值转换为带有UTC时区的LocalDateTime。支持的值有:Date、Instant、LocalDate、LocalDateTime(直接返回)、ISO8601格式的String或null。
LocalDateTime asLocalDateTime(Object value, String timeZoneId)将值转换为给定时区的LocalDateTime。支持的值有:Date、Instant、LocalDate、LocalDateTime(直接返回)、ISO8601格式的String或null。
Date asDate(Object value)将值转换为UTC时区中的Date。支持的值有Instant、LocalDate、LocalDateTime、ISO8601格式的String或null。
Object atTime(Object value, int hours, int minutes, int seconds)在UTC时区中将值的时间设置为指定的小时、分钟和秒。支持的值有Date、Instant、LocalDateTime或ISO8601格式的String。返回值将与输入类型相同,对于字符串,根据提供的字符串格式,将是Instant或LocalDateTime。
Object atTimeWithTimeZone(Object value, int hours, int minutes, int seconds, String timeZoneId)在给定时区中将值的时间设置为指定的小时、分钟和秒。支持的值有Date、Instant、LocalDateTime或ISO8601格式的String。返回值将与输入类型相同,对于字符串,根据提供的字符串格式,将是Instant或LocalDateTime。
Object atTimeZone(Object value, String timeZoneId)返回指定时区的Instant。支持的值有Date或Instant。仅将此用于显示目的,切勿将日期存储在UTC以外的其他时区中。
List<String> getAvailableTimeZoneIds()返回可用时区ID的列表。更完整的列表也可以在这里找到。
int getField(Object value, String chronoFieldString)通过指定ChronoField作为字符串来获取"时间片段"。chrono字段可以指定为显示名称或枚举名称。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。所有chrono字段的列表可以在这里找到。例如:flw.time.getField(flw.time.now(), 'DAY_OF_WEEK')或flw.time.getField(flw.time.now(), 'AlignedWeekOfYear')。
boolean isWeekend(Object value)确定给定值是否表示周末。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Instant fullDateTimeInstant(int year, int month, int day, int hour, int minute, int second)使用给定的值在UTC时区创建Instant。
Date fullDateTimeDate(int year, int month, int day, int hour, int minute, int second)使用给定的值创建Date。
Object plusSeconds(Object value, long seconds)向给定值添加秒数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusMinutes(Object value, long minutes)向给定值添加分钟数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusHours(Object value, long hours)向给定值添加小时数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusDays(Object value, long days)向给定值添加天数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusWeeks(Object value, long days)向给定值添加周数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusMonths(Object value, long days)向给定值添加月数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusYears(Object value, long days)向给定值添加年数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object plusDuration(Object value, String iso8601Duration)向给定值添加ISO8601编码的持续时间。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。例如:flw.time.plusDuration(flw.time.now(), 'P1Y')(向现在添加一年),flw.time.plusDuration(flw.time.now(), 'P14D')(向现在添加14天),flw.time.plusDuration(flw.time..now(), 'PT30M10S')(向现在添加30分钟和10秒)。
Object minusSeconds(Object value, long seconds)从给定值减去秒数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusMinutes(Object value, long minutes)从给定值减去分钟数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusHours(Object value, long hours)从给定值减去小时数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusDays(Object value, long days)从给定值减去天数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusWeeks(Object value, long days)从给定值减去周数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusMonths(Object value, long days)从给定值减去月数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusYears(Object value, long days)从给定值减去年数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
Object minusDuration(Object value, String iso8601Duration)从给定值减去ISO8601编码的持续时间。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。例如:flw.time.minusDuration(flw.time.now(), 'P1Y')(从现在减去一年),flw.time.minusDuration(flw.time.now(), 'P14D')(从现在减去14天),flw.time.minusDuration(flw.time..now(), 'PT30M10S')(从现在减去30分钟和10秒)。
long secondsOfDuration(String iso8601Duration)返回ISO持续时间字符串中的秒数,例如PT60M返回3600。
boolean isBefore(Object firstValue, Object secondValue)检查第一个值是否在第二个值之前。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。这些值不需要是相同类型,如果需要,它们将自动转换。
boolean isBeforeOrEqual(Object firstValue, Object secondValue)检查第一个值是否在第二个值之前或等于第二个值。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。这些值不需要是相同类型,如果需要,它们将自动转换。
boolean isAfter(Object firstValue, Object secondValue)检查第一个值是否在第二个值之后。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。这些值不需要是相同类型,如果需要,它们将自动转换。
boolean isAfterOrEqual(Object firstValue, Object secondValue)检查第一个值是否在第二个值之后或等于第二个值。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。这些值不需要是相同类型,如果需要,它们将自动转换。
boolean areEqual(Object firstValue, Object secondValue)检查第一个值是否等于第二个值。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。这些值不需要是相同类型,如果需要,它们将自动转换。
boolean isBeforeTime(Object value, String timeZoneId, int hours, int minutes, int seconds)检查给定值是否在给定时区的某个时间之前。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
boolean isAfterTime(Object value, String timeZoneId, int hours, int minutes, int seconds)检查给定值是否在给定时区的某个时间之后。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long getFieldFromDurationBetweenDates(Object firstValue, Object secondValue, String chronoUnitString)返回两个值之间的持续时间。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。支持以下单位:Nanos、Micros、Millis、Seconds、Hours、HalfDays、Days、Weeks、Months、Years、Decades、Centuries、Millenia。请注意,数字始终是long类型,即数字始终会被四舍五入。
long secondsBetween(Object firstValue, Object secondValue)返回两个值之间的秒数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long minutesBetween(Object firstValue, Object secondValue)返回两个值之间的分钟数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long hoursBetween(Object firstValue, Object secondValue)返回两个值之间的小时数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long daysBetween(Object firstValue, Object secondValue)返回两个值之间的天数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long weeksBetween(Object firstValue, Object secondValue)返回两个值之间的周数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long monthsBetween(Object firstValue, Object secondValue)返回两个值之间的月数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long yearsBetween(Object firstValue, Object secondValue)返回两个值之间的年数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。
long getTimeZoneOffset(Object value, String timeZoneId)计算指定时区中特定时间点与UTC的偏移秒数。支持的值有Date、Instant、LocalDate、LocalDateTime或ISO8601格式的String。

字符串工具

脚本API还支持字符串处理的实用工具,可通过flw.string访问。

您可以使用与表达式中相同的功能,使用flwStringUtils。

以下是可用函数的列表:

函数描述
String toUpperCase(Object text)将对象的所有字母转换为大写。支持的值有String、Json TextNode或null。
String toLowerCase(Object text)将对象的所有字母转换为小写。支持的值有String、Json TextNode或null。
String capitalize(Object text)将文本首字母大写,即将第一个字母设置为大写。支持的值有String、Json TextNode或null。
String trimWhitespace(Object text)删除文本中所有前导和尾随空格。支持的值有String、Json TextNode或null。
boolean hasText(Object text)检查字符串是否包含文本(不为null或空)。支持的值有String、Json TextNode或null。
boolean contains(Object text, String otherText)检查字符串是否包含另一个字符串。支持的值有String、Json TextNode或null。
boolean containsIgnoreCase(Object text, String otherText)检查字符串是否包含另一个字符串,忽略大小写。支持的值有String、Json TextNode或null。
boolean matches(Object text, String regularExpression)检查文本是否匹配正则表达式。支持的值有String、Json TextNode或null。
boolean equals(Object text, String otherText)检查两个字符串是否相等。支持的值有String、Json TextNode或null。
boolean equalsIgnoreCase(Object text, String otherText)检查两个字符串是否相等,忽略大小写。支持的值有String、Json TextNode或null。
String substring(Object text, int from, int to)返回提供的字符范围内的子字符串。索引基于0,from包含在内,to不包含在内。支持的值有String、Json TextNode或null。
String substringFrom(Object text, int from)返回从给定位置开始的文本(索引基于0)。支持的值有String、Json TextNode或null。
List<String> split(Object text, String delimiter)使用给定的分隔符将文本拆分为集合。分隔符可以是单个字符,例如分号,或正则表达式。支持的值有String、Json TextNode或null。
String join(Collection<String> collection, String delimiter)使用给定的分隔符将列表或集合的所有条目连接为单个字符串。支持的值有String、Json TextNode或null。
String newline()返回新行/换行符。
String carriageReturn()返回回车符。
String escapeHtml(Object text)使用HTML实体转义对象中的字符。支持的值有String、Json TextNode或null。
String unescapeHtml(Object text)将包含实体转义的字符串转义为包含与转义对应的实际Unicode字符的字符串。支持的值有String、Json TextNode或null。

数学工具

脚本API还支持数学运算工具,可通过flw.math访问。

您可以使用与表达式中相同的功能,使用flwMathUtils。

以下是可用函数的列表:

函数描述
double sum(Collection<Double> numbers)计算数字列表的总和。
double average(Collection<Double> numbers)计算数字列表的平均值。
double floor(double number)返回提供的数字的下一个较低整数。
double ceil(double number)返回提供的数字的下一个较高整数。
double round(double number)将数字四舍五入为整数值。
double round(double number, int scale)使用RoundingMode#HALF_UP将数字四舍五入到最多小数位数。
double min(Collection<Double> numbers)返回数字列表中的最小数字。
double max(Collection<Double> numbers)返回数字列表中的最大数字。
double abs(double number)返回数字的绝对值。
double median(Collection<Double> numbers)返回数字列表的中位数。
double parseDouble(String string)将字符串转换为double值。
int parseInt(String string)将字符串转换为整数值。

区域工具

脚本API还提供用于处理区域设置、国家和语言的工具,可通过flw.locale访问。

您可以使用与表达式中相同的功能,使用flwLocaleUtils。

以下是可用函数的列表:

函数描述
Locale getLocaleForLanguageTag(String languageTag)返回具有给定语言标签的区域设置,例如'ch-DE'。
List<Locale> getAvailableLocales()返回可用区域设置的列表。
Locale getDefaultLocale()返回系统默认区域设置。
List<String> getAllCountryCodes()返回所有2字母ISO国家代码的列表。
List<String> getAllLanguageCodes()返回所有ISO语言代码的列表,例如"de"(不是"de-CH")。
String getLanguageDisplayName(String languageIsoCode, String displayLanguageTag)返回某种语言的单一语言名称,例如"German"或"Spanish"。
String getCountryDisplayName(String languageTag, String displayLanguageTag)返回某种语言的单一国家名称,例如"Switzerland"或"Germany"。
List<String> getAllLanguageDisplayNames(String displayLanguageTag)返回某种语言中所有语言名称的列表。
List<String> getAllCountryDisplayNames(String displayLanguageTag)返回某种语言中所有国家名称的列表。

格式化工具

脚本API还提供围绕格式化的工具,可通过flw.format访问。

您可以使用与表达式中相同的功能,使用flwFormatUtils。

以下是可用函数的列表:

函数描述
String formatString(String text, Object... substitutes)根据Java格式化程序规范格式化字符串,参见这里或这里。
String formatDate(Object value, String dateFormat)使用给定格式将值格式化为字符串。支持的值有Date、Instant、LocalDate、LocaDateTime或ISO8601格式的字符串。
String formatDecimal(String pattern, double decimal)使用默认区域设置中的十进制格式化程序根据Java格式化程序规范格式化字符串。参见格式化规范。
String formatStringWithLocale(String languageTag, String text, Object... substitutes)根据Java格式化程序规范格式化字符串。字符串根据提供的语言标签的区域设置中指定的格式进行格式化。参见这里或这里。
String formatCurrencyWithLocale(String currencyCode, double amount, String languageTag)根据提供的语言标签的区域设置中指定的格式格式化货币金额。

​​AI赋能Flowable:破解传统BPM的响应延迟与决策盲区​​

April 3, 2025

Flowable AI Cover

在数字化转型的浪潮中,业务流程管理(BPM)正从传统的规则驱动向智能化跃迁。Flowable作为轻量级、高性能的流程引擎,凭借其灵活性和扩展性,正在与人工智能(AI)技术深度结合,推动企业运营效率的颠覆性提升。2024年9月,Flowable推出了Flowable AI Studio,引入了一系列AI辅助功能,旨在提升工作流自动化和智能化。这些功能包括生成式AI建模、AI服务和内容分析,旨在解决传统建模中的"空白画布"问题,并增强运行时的决策能力。

AI功能详解

Flowable AI Studio通过REST API和Java集成实现了一系列强大的AI功能,这些功能可以轻松嵌入现有应用或作为云服务运行。以下是核心功能详解:

功能类型功能描述应用价值实际案例
AI辅助建模通过自然语言提示生成流程、案例、表单和应用程序的初稿显著加速建模过程根据业务描述自动生成完整的客户服务工作流模型
AI服务集成将AI能力无缝集成到业务流程中创建智能、自适应的工作流系统基于实时数据智能调整审批流程路径
智能内容管理提供内容摘要、情感分析、自动翻译等能力增强文档处理和内容分析能力自动生成业务报告摘要,实时分析客户反馈情绪
生成式AI建模提供全方位的模型创建支持解决建模起点难题,降低入门门槛快速生成新项目的初始流程图和相关配置
分析性AI决策提供实时决策支持和流程优化建议提升运营效率和资源利用率基于AI预测结果优化任务分配和资源调度

应用场景分析

1. 智能客户入职流程

在金融与电信行业中,AI赋能的客户入职流程展现出显著优势:

  • 智能数据分析:自动处理信用评分和收入证明
  • 风险评估:基于AI模型进行实时风险评估
  • 动态流程:根据评估结果自动调整审批路径
  • 效率提升:显著减少人工干预,加快处理速度

2. 智能文档处理系统

在法律和医疗领域,AI驱动的文档处理带来革新性变化:

  • 自动分类:智能识别和分类各类专业文档
  • 信息提取:准确提取文档中的关键信息
  • 智能路由:自动将文档分发至相关处理部门
  • 流程自动化:实现发票等标准文档的自动处理流程

3. 预测性设备维护

在制造业场景中,AI与工作流的结合优化了设备维护流程:

  • 故障预测:通过AI分析设备运行数据
  • 预警机制:及时发现潜在问题
  • 自动调度:智能安排维护人员和资源
  • 效益提升:有效减少设备停机时间

技术实现路径与开发指南

要实现Flowable的AI赋能,开发者需关注以下关键点:

1. AI模型与流程引擎的松耦合设计

通过REST API或消息队列集成AI服务,避免紧耦合带来的维护成本。例如:

@Service
public class AIServiceIntegration {
    @Autowired
    private RestTemplate restTemplate;
    
    public AIAnalysisResult analyzeProcess(ProcessData data) {
        return restTemplate.postForObject("/ai/analyze", data, AIAnalysisResult.class);
    }
}

2. 动态变量与上下文感知

利用Flowable的流程变量(execution variables)传递AI分析结果,例如通过runtimeService.setVariable()动态更新决策参数:

@Service
public class AIDecisionService {
    @Autowired
    private RuntimeService runtimeService;
    
    public void updateProcessDecision(String executionId, AIDecision decision) {
        runtimeService.setVariable(executionId, "aiDecision", decision);
    }
}

3. 事件监听与实时反馈

通过TaskListener和ExecutionListener捕获流程节点事件,触发AI模型的再训练与优化:

public class AIFeedbackListener implements TaskListener {
    @Override
    public void notify(DelegateTask task) {
        // 收集任务完成数据用于AI模型优化
        AIFeedbackData feedback = collectTaskData(task);
        aiModelService.updateModel(feedback);
    }
}

未来展望:AI与流程引擎的共生演进

随着大语言模型(LLM)的普及,Flowable的智能化将呈现新趋势:

1. 自然语言驱动的流程设计

用户可通过对话描述业务需求,AI自动生成BPMN流程图。这种方式将大大降低流程建模的门槛,使业务人员能够更直接地参与流程设计。例如,通过简单的描述:"创建一个客户订单审批流程,包含销售审核、财务审核和最终确认三个步骤",AI就能自动生成对应的BPMN模型。

2. 自适应流程优化

基于强化学习(RL)的AI模型持续迭代流程规则,实现"越用越智能"的闭环。系统将通过分析历史数据、执行效率和业务成功率,自动调整流程路径和决策规则,不断优化流程执行效果。

3. 人机协同的混合流程

AI处理标准化任务,复杂场景无缝转交人工,形成双向交互机制。这种协同模式将充分发挥AI在处理重复性工作中的效率优势,同时保留人类在处理复杂决策时的判断能力。具体表现为:

  • AI自动处理数据录入、文档分类等标准化任务
  • 遇到异常或复杂情况时,智能转交给人工处理
  • 人工处理过程中的决策数据被用来训练AI模型,提高其处理能力

总结

Flowable AI Studio的推出标志着BPM领域进入智能化新纪元。通过AI技术的深度整合,Flowable不仅提升了流程自动化水平,更为企业数字化转型提供了强大动力。随着技术持续演进,Flowable必将在智能流程管理领域发挥更大价值。

Flowable 后端表达式

April 2, 2025

后端表达式

简介

后端表达式是一种在执行流程、案例或决策表期间获取或设置变量的工具。后端表达式还可以用于实现自定义逻辑,或将该逻辑委托给部署在服务器上的Java服务。使用表达式的示例包括设置动态任务名称、网关后序列流的评估,或创建特定类型的变量。顾名思义,这些表达式在执行期间在后端运行。因此,在评估时它们无法访问前端数据。

在Flowable Design中,表达式可以在带有闪电图标的字段中使用:⚡️

Flowable使用统一表达式语言(UEL)来解析表达式。UEL的文档是了解语法和可用运算符的良好参考。每个表达式都以${开始,以}结束。

表达式分为两种类型:

  1. 值表达式:提供一个值。支持的值包括布尔值、字符串、整数、浮点数和null。典型的值表达式形式为${variable.property}或${bean.property}。

  2. 方法表达式:调用带参数或不带参数的方法。方法表达式的示例为${bean.setPropertyValue('newValue')}。为了区分不带参数的值表达式和方法表达式,在方法调用的末尾使用空括号。例如:${variable.toString()}。

理论上,任何暴露给应用程序的Spring bean都可以在后端表达式中使用,但并非所有类都能以允许正确表达式评估的方式进行序列化。

信息提示 某些表达式可在脚本中使用(例如在BPMN或CMMN脚本任务中)。可用表达式的列表可在后端脚本文档中找到。

关键字

流程和CMMN引擎支持上下文相关的对象。两种引擎都可以解析层次对象,如root和parent,并可用于访问它们的本地属性。root代表执行层次结构顶部的对象,可以是流程实例或案例实例。parent是对当前对象在层次结构中紧邻上方的第一个流程或案例实例的引用。

对于流程实例和案例实例,definition提供对定义及其所有属性的访问。

在BPMN执行上下文中,额外的保留关键字包括execution和authenticatedUserId:

  • execution:引用当前的"执行路径"或"令牌"。它提供对name、businessKey、processDefinitionId和parentExecution等属性的访问(在技术上,它由ExecutionEntity类支持)。

  • task:引用当前任务(在技术上,它由TaskEntity类支持)。

  • authenticatedUserId:当前认证用户的字符串标识符。如果没有认证用户,该变量将不可用。

CMMN引擎上下文支持task(见上文)、caseInstance和planItemInstance:

  • caseInstance:引用当前案例实例。它提供对name、state、businessKey、caseDefinitionId和parentId等属性的访问(在技术上,它由CaseInstanceEntity类支持)。

  • planItemInstance:引用与案例关联的计划项实例。

表达式概述

流程实例属性

以下属性在流程实例上可用,例如当使用root或parent时。

示例用法:${root.businessKey}

属性描述
businessKey流程实例的业务键(如果已设置)
businessStatus流程实例的业务状态(如果已设置)
callbackId如果流程实例是通过CMMN案例实例中的流程任务启动的,此值将引用关联的计划项实例的ID
description流程实例的描述(如果已设置)
definition流程定义。详细信息见下文
id流程实例的唯一技术标识符
name流程实例的名称(如果已设置)
startTime流程实例启动的时间(java.util.Date类型)
startUserId启动此流程实例的用户ID
tenantId此流程实例的租户标识符

案例实例属性

以下属性在案例实例上可用,例如当使用root或parent时。

示例用法:${parent.businessStatus}

属性描述
businessKey案例实例的业务键(如果已设置)
businessStatus案例实例的业务状态(如果已设置)
callbackId如果案例实例是通过CMMN案例实例中的案例任务启动的,此值将引用关联的计划项实例的ID
description案例实例的描述(如果已设置)
definition案例定义。详细信息见下文
id案例实例的唯一技术标识符
name案例实例的名称(如果已设置)
startTime案例实例启动的时间(java.util.Date类型)
startUserId启动此案例实例的用户ID
tenantId此案例实例的租户标识符

定义属性

属性描述
id定义的唯一技术标识符
category定义的类别(如果在相关模型中设置)
deploymentId与此定义相关的部署引用。查看部署文档以了解更多信息
description定义的描述(如果在模型中配置)
key定义的键,对于所有类似的模型类型必须是唯一的。对于给定的键,通常在运行时系统中部署多个版本
name定义的名称,与BPMN或CMMN模型的名称匹配
tenantId部署此定义的租户标识符
version定义的版本。每次发布模型时,都会为相同的键创建一个新定义,版本等于最后版本 + 1

变量函数

变量操作是表达式的重要用例。

设置变量可以通过runtimeService完成:

${runtimeService.setVariable(processInstanceId, variableName, variableValue)}
${cmmnRuntimeService.setVariable(caseInstanceId, variableName, variableValue)}

或在关键字对象上(取决于上下文):

  • ${execution.setVariable(varName, varValue)} 设置当前流程实例的变量varName的值为varValue
  • ${caseInstance.setVariable(varName, varValue)} 设置当前案例实例的变量varName的值为varValue
  • ${planItemInstance.setVariable(varName, varValue)} 设置当前案例实例的变量varName的值为varValue
  • ${task.setVariable(varName, varValue)} 设置当前流程/案例实例的变量varName的值为varValue

以下函数可用于处理变量,适用于BPMN和CMMN上下文:

函数描述
${var:get(varName)}获取变量的值。与直接在表达式中写变量名的主要区别是,使用此函数在变量不存在时不会抛出异常
${var:getOrDefault(varName, defaultValue)}获取变量值,如果变量不存在则返回默认值
${var:exists(varName)}检查变量是否存在
${var:isEmpty(varName)}检查变量值是否为空。根据变量类型,行为如下:对于String变量,如果是空字符串则为空;对于java.util.Collection变量,如果集合没有元素则为真;对于ArrayNode变量,如果没有元素则为真。如果变量为null,始终返回真
${var:isNotEmpty(varName)}isEmpty的反向操作
${var:equals(varName, value)}检查变量是否等于给定值。这是${execution.getVariable("varName") != null && execution.getVariable("varName") == value}的简写函数
${var:notEquals(varName, value)}equals的反向操作
${var:contains(varName, value1, value2, ...)}检查所有提供的值是否包含在变量中。根据变量类型有不同的行为
${var:containsAny(varName, value1, value2, ...)}类似于contains函数,但如果任何(而不是所有)传递的值包含在变量中,则返回true
${var:base64(varName)}将Binary或String变量转换为Base64字符串
${var:lowerThan(varName, value)}${execution.getVariable("varName") != null && execution.getVariable("varName") < value}的简写。别名 = lt
${var:lowerThanOrEquals(varName, value)}类似,但用于 <= 。别名 = lte
${variables:greaterThan(varName, value)}类似,但用于 > 。别名 = gr
${variables:greaterThanOrEquals(varName, value)}类似,但用于 >= 。别名 = gte

流程分配函数

以下函数在BPMN流程执行上下文中可用:

函数描述
${bpmn:setAssignee(processInstanceId, userId)}将指定processInstanceId的流程的受理人设置为给定的userId
${bpmn:getAssignee()}返回当前流程的受理人
${bpmn:getAssignee(processInstanceId)}返回指定processInstanceId的流程的受理人
${bpmn:removeAssignee()}移除当前流程的受理人
${bpmn:removeAssignee(processInstanceId)}移除指定processInstanceId的流程的受理人
${bpmn:setOwner(processInstanceId, userId)}将指定processInstanceId的流程的所有者设置为给定的userId
${bpmn:getOwner()}返回当前流程的所有者
${bpmn:getOwner(processInstanceId)}返回指定processInstanceId的流程的所有者
${bpmn:removeOwner()}移除当前流程的所有者
${bpmn:removeOwner(processInstanceId)}移除指定processInstanceId的流程的所有者
${bpmn:addParticipantUser(processInstanceId, userId)}将给定userId的用户作为参与者添加到指定processInstanceId的流程中
${bpmn:addParticipantUsers(processInstanceId, userIds)}将给定userIds的用户作为参与者添加到指定processInstanceId的流程中。userIds可以是ArrayNode、Iterable或逗号分隔的userId列表
${bpmn:removeParticipantUser(processInstanceId, userId)}从指定processInstanceId的流程中移除给定userId的参与者用户
${bpmn:removeParticipantUsers(processInstanceId, userIds)}从指定processInstanceId的流程中移除给定userIds的参与者用户。userIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${bpmn:addCandidateUser(processInstanceId, userId)}将给定userId的用户作为候选人添加到指定processInstanceId的流程中
${bpmn:addCandidateUsers(processInstanceId, userIds)}将给定userIds的用户作为候选人添加到指定processInstanceId的流程中。userIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${bpmn:removeCandidateUser(processInstanceId, userId)}从指定processInstanceId的流程中移除给定userId的候选人用户
${bpmn:removeCandidateUsers(processInstanceId, userIds)}从指定processInstanceId的流程中移除给定userIds的候选人用户。userIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${bpmn:addCandidateGroup(processInstanceId, groupId)}将给定groupId的组作为候选组添加到指定processInstanceId的流程中
${bpmn:addCandidateGroups(processInstanceId, groupIds)}将给定groupIds的组作为候选组添加到指定processInstanceId的流程中。groupIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${bpmn:removeCandidateGroup(processInstanceId, groupId)}从指定processInstanceId的流程中移除给定groupId的候选组
${bpmn:removeCandidateGroups(processInstanceId, groupIds)}从指定processInstanceId的流程中移除给定groupIds的候选组。groupIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串

案例分配函数

以下函数在CMMN案例执行上下文中可用:

函数描述
${cmmn:setAssignee(caseInstanceId, userId)}将指定caseInstanceId的案例的受理人设置为给定的userId
${cmmn:getAssignee()}返回当前案例的受理人
${cmmn:getAssignee(caseInstanceId)}返回指定caseInstanceId的案例的受理人
${cmmn:removeAssignee()}移除当前案例的受理人
${cmmn:removeAssignee(caseInstanceId)}移除指定caseInstanceId的案例的受理人
${cmmn:setOwner(caseInstanceId, userId)}将指定caseInstanceId的案例的所有者设置为给定的userId
${cmmn:getOwner()}返回当前案例的所有者
${cmmn:getOwner(caseInstanceId)}返回指定caseInstanceId的案例的所有者
${cmmn:removeOwner()}移除当前案例的所有者
${cmmn:removeOwner(caseInstanceId)}移除指定caseInstanceId的案例的所有者
${cmmn:addParticipantUser(caseInstanceId, userId)}将给定userId的用户作为参与者添加到指定caseInstanceId的案例中
${cmmn:addParticipantUsers(caseInstanceId, userIds)}将给定userIds的用户作为参与者添加到指定caseInstanceId的案例中。userIds可以是ArrayNode、Iterable或逗号分隔的userId列表
${cmmn:removeParticipantUser(caseInstanceId, userId)}从指定caseInstanceId的案例中移除给定userId的参与者用户
${cmmn:removeParticipantUsers(caseInstanceId, userIds)}从指定caseInstanceId的案例中移除给定userIds的参与者用户。userIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${cmmn:addCandidateUser(caseInstanceId, userId)}将给定userId的用户作为候选人添加到指定caseInstanceId的案例中
${cmmn:addCandidateUsers(caseInstanceId, userIds)}将给定userIds的用户作为候选人添加到指定caseInstanceId的案例中。userIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${cmmn:removeCandidateUser(caseInstanceId, userId)}从指定caseInstanceId的案例中移除给定userId的候选人用户
${cmmn:removeCandidateUsers(caseInstanceId, userIds)}从指定caseInstanceId的案例中移除给定userIds的候选人用户。userIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${cmmn:addCandidateGroup(caseInstanceId, groupId)}将给定groupId的组作为候选组添加到指定caseInstanceId的案例中
${cmmn:addCandidateGroups(caseInstanceId, groupIds)}将给定groupIds的组作为候选组添加到指定caseInstanceId的案例中。groupIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串
${cmmn:removeCandidateGroup(caseInstanceId, groupId)}从指定caseInstanceId的案例中移除给定groupId的候选组
${cmmn:removeCandidateGroups(caseInstanceId, groupIds)}从指定caseInstanceId的案例中移除给定groupIds的候选组。groupIds可以是TextNode的ArrayNode、String的Iterable或逗号分隔的userId字符串

CMMN函数

以下函数在CMMN上下文中可用:

函数描述
${cmmn:copyLocalVariable(planItemInstance, localVariableName, index, variableName)}将计划项实例的本地变量复制到案例实例
${cmmn:getTask(taskId)}返回具有提供的ID的任务(运行时或历史)
${cmmn:isPlanItemCompleted(planItemInstance)}此函数评估计划项是否已完成,这很可能用于具有重复规则的计划项,以检查它是否已经完成
${cmmn:isStageCompletable()}返回当前案例阶段是否可完成(取决于阶段及其元素的配置,参见"自动完成"等)
${cmmn:replaceVariableInList(planItemInstance, variableName, index, listVariableName)}使用variableName引用的变量值更改具有给定listVariableName的列表中提供的索引处的条目
${cmmn:setBusinessStatus(caseInstanceId, businessStatus)}v3.16.0+ 更改匹配提供的caseInstanceId的案例实例的业务状态属性
${cmmn:triggerCaseEvaluation(<parameter>)}触发案例实例的案例实例哨兵的评估。参数可以是planItemInstance、caseInstance或案例实例的ID

BPMN函数

以下函数在BPMN上下文中可用:

函数描述
${bpmn:copyLocalVariable(localVariableName, variableName)}将执行本地变量复制到流程实例
${bpmn:copyLocalVariableToParent(localVariableName, variableName)}将执行本地变量复制到其父执行
${bpmn:getTask(taskId)}返回具有提供的ID的任务(运行时或历史)
${bpmn:replaceVariableInList(variableName, index, listVariableName)}使用variableName引用的变量值更改具有给定listVariableName的列表中提供的索引处的条目
${bpmn:setBusinessStatus(processInstanceId, businessStatus)}v3.16.0+ 更改匹配提供的processInstanceId的流程实例的业务状态属性
${bpmn:triggerCaseEvaluation()}触发作为流程实例父级的案例实例的案例实例哨兵的评估

字符串工具函数

字符串操作方法是表达式的经典用例。以下方法可用:

表达式描述
${flwStringUtils.carriageReturn()}返回回车字符
${flwStringUtils.capitalize(text)}将文本首字母大写。支持的值类型有String、Json TextNode或null
${flwStringUtils.contains(text, otherText)}检查字符串是否包含另一个字符串。支持的值类型有String、Json TextNode或null
${flwStringUtils.containsIgnoreCase(text, otherText)}检查字符串是否包含另一个字符串(忽略大小写)。支持的值类型有String、Json TextNode或null
${flwStringUtils.equals(text, otherText)}检查两个字符串是否相等。支持的值类型有String、Json TextNode或null
${flwStringUtils.equalsIgnoreCase(text, otherText)}检查两个字符串是否相等(忽略大小写)。支持的值类型有String、Json TextNode或null
${flwStringUtils.escapeHtml(text)}使用HTML实体转义对象中的字符。支持的值类型有String、Json TextNode或null
${flwStringUtils.hasText(text)}检查字符串是否包含文本(不为null或空)。支持的值类型有String、Json TextNode或null
${flwStringUtils.join(collection, String delimiter)}使用给定的分隔符将列表或集合的所有条目连接成单个字符串。支持的值类型有String、Json TextNode或null
${flwStringUtils.matches(text, regularExpression)}检查文本是否匹配正则表达式。支持的值类型有String、Json TextNode或null
${flwStringUtils.newline()}返回换行符
${flwStringUtils.substring(text, from, to)}返回提供的字符范围内的子字符串。索引从0开始,from包含在内,to不包含在内。支持的值类型有String、Json TextNode或null
${flwStringUtils.substringFrom(text, from)}返回从给定位置开始的文本(索引从0开始)。支持的值类型有String、Json TextNode或null
${flwStringUtils.split(text, delimiter)}使用给定的分隔符将文本分割成集合。分隔符可以是单个字符(如分号)或正则表达式。支持的值类型有String、Json TextNode或null
${flwStringUtils.toLowerCase(text)}将字符串的所有字符转换为小写
${flwStringUtils.toUpperCase(text)}将字符串的所有字符转换为大写
${flwStringUtils.trimWhitespace(text)}删除文本中的所有前导和尾随空格。支持的值类型有String、Json TextNode或null
${flwStringUtils.unescapeHtml(text)}将包含实体转义的字符串转换为包含对应Unicode字符的字符串。支持的值类型有String、Json TextNode或null

日期和时间工具函数

以下是可用的日期和时间操作函数:

基本操作

表达式描述
${flwTimeUtils.now()}返回UTC时区的当前日期和时间
${flwTimeUtils.currentDate()}返回UTC时区00:00 AM的当前日期,返回类型为Instant
${flwTimeUtils.currentLocalDate()}返回系统时区(UTC)的当前日期,返回类型为LocalDate
${flwTimeUtils.currentLocalDateTime()}返回系统时区(UTC)的当前日期和时间,返回类型为LocalDateTime

转换函数

表达式描述
${flwTimeUtils.instantFromTimestamp(long timestamp)}从Unix时间戳(自1970年1月1日起的秒数)创建Instant
${flwTimeUtils.dateFromTimestamp(long timestamp)}从Unix时间戳创建Date
${flwTimeUtils.parseInstant(Date date)}将Date转换为Instant
${flwTimeUtils.parseInstant(String instantIsoString)}将ISO8601格式的字符串解析为Instant
${flwTimeUtils.parseInstant(Object value, String pattern)}使用指定模式将对象解析为Instant
${flwTimeUtils.parseLocalDate(Object value, String pattern)}使用指定模式将对象解析为LocalDate
${flwTimeUtils.parseLocalDateTime(Object value, String pattern)}使用指定模式将对象解析为LocalDateTime

类型转换函数

表达式描述
${flwTimeUtils.asInstant(Object value)}将值转换为UTC时区的Instant
${flwTimeUtils.asInstant(Object value, String timeZoneId)}将值转换为指定时区的Instant
${flwTimeUtils.asLocalDate(Object value)}将值转换为UTC时区的LocalDate
${flwTimeUtils.asLocalDate(Object value, String timeZoneId)}将值转换为指定时区的LocalDate
${flwTimeUtils.asLocalDateTime(Object value)}将值转换为UTC时区的LocalDateTime
${flwTimeUtils.asLocalDateTime(Object value, String timeZoneId)}将值转换为指定时区的LocalDateTime
${flwTimeUtils.asDate(Object value)}将值转换为UTC时区的Date

时间操作函数

表达式描述
${flwTimeUtils.atTime(Object value, int hours, int minutes, int seconds)}在UTC时区设置指定时间的小时、分钟和秒
${flwTimeUtils.atTimeWithTimeZone(Object value, int hours, int minutes, int seconds, String timeZoneId)}在指定时区设置指定时间的小时、分钟和秒
${flwTimeUtils.atTimeZone(Object value, String timeZoneId)}返回指定时区的Instant(仅用于显示,不要用于存储)
${flwTimeUtils.getAvailableTimeZoneIds()}返回可用时区ID列表
${flwTimeUtils.getField(Object value, String chronoFieldString)}获取指定ChronoField的时间片段
${flwTimeUtils.isWeekend(Object value)}判断给定值是否为周末

日期时间创建函数

表达式描述
${flwTimeUtils.fullDateTimeInstant(int year, int month, int day, int hour, int minute, int second)}在UTC时区创建具有给定值的Instant
${flwTimeUtils.fullDateTimeDate(int year, int month, int day, int hour, int minute, int second)}创建具有给定值的Date

时间增加函数

表达式描述
${flwTimeUtils.plusSeconds(Object value, long seconds)}添加秒数
${flwTimeUtils.plusMinutes(Object value, long minutes)}添加分钟数
${flwTimeUtils.plusHours(Object value, long hours)}添加小时数
${flwTimeUtils.plusDays(Object value, long days)}添加天数
${flwTimeUtils.plusWeeks(Object value, long weeks)}添加周数
${flwTimeUtils.plusMonths(Object value, long months)}添加月数
${flwTimeUtils.plusYears(Object value, long years)}添加年数
${flwTimeUtils.plusDuration(Object value, String iso8601Duration)}添加ISO8601编码的持续时间

时间减少函数

表达式描述
${flwTimeUtils.minusSeconds(Object value, long seconds)}减少秒数
${flwTimeUtils.minusMinutes(Object value, long minutes)}减少分钟数
${flwTimeUtils.minusHours(Object value, long hours)}减少小时数
${flwTimeUtils.minusDays(Object value, long days)}减少天数
${flwTimeUtils.minusWeeks(Object value, long weeks)}减少周数
${flwTimeUtils.minusMonths(Object value, long months)}减少月数
${flwTimeUtils.minusYears(Object value, long years)}减少年数
${flwTimeUtils.minusDuration(Object value, String iso8601Duration)}减少ISO8601编码的持续时间

时间比较和计算函数

表达式描述
${flwTimeUtils.secondsOfDuration(String iso8601Duration)}返回ISO持续时间字符串中的秒数
${flwTimeUtils.isBefore(Object firstValue, Object secondValue)}检查第一个值是否在第二个值之前
${flwTimeUtils.isBeforeOrEqual(Object firstValue, Object secondValue)}检查第一个值是否在第二个值之前或等于第二个值
${flwTimeUtils.isAfter(Object firstValue, Object secondValue)}检查第一个值是否在第二个值之后
${flwTimeUtils.isAfterOrEqual(Object firstValue, Object secondValue)}检查第一个值是否在第二个值之后或等于第二个值
${flwTimeUtils.areEqual(Object firstValue, Object secondValue)}检查两个值是否相等
${flwTimeUtils.isBeforeTime(Object value, String timeZoneId, int hours, int minutes, int seconds)}检查给定值是否在指定时区的某个时间之前
${flwTimeUtils.isAfterTime(Object value, String timeZoneId, int hours, int minutes, int seconds)}检查给定值是否在指定时区的某个时间之后

时间间隔计算函数

表达式描述
${flwTimeUtils.getFieldFromDurationBetweenDates(Object firstValue, Object secondValue, String chronoUnitString)}返回两个值之间的持续时间
${flwTimeUtils.secondsBetween(Object firstValue, Object secondValue)}返回两个值之间的秒数
${flwTimeUtils.minutesBetween(Object firstValue, Object secondValue)}返回两个值之间的分钟数
${flwTimeUtils.hoursBetween(Object firstValue, Object secondValue)}返回两个值之间的小时数
${flwTimeUtils.daysBetween(Object firstValue, Object secondValue)}返回两个值之间的天数
${flwTimeUtils.weeksBetween(Object firstValue, Object secondValue)}返回两个值之间的周数
${flwTimeUtils.monthsBetween(Object firstValue, Object secondValue)}返回两个值之间的月数
${flwTimeUtils.yearsBetween(Object firstValue, Object secondValue)}返回两个值之间的年数
${flwTimeUtils.getTimeZoneOffset(Object value, String timeZoneId)}计算指定时区的特定时间点与UTC的秒数偏移量

注意:对于大多数函数,支持的值类型包括Date、Instant、LocalDate、LocalDateTime或ISO8601格式的字符串。不同类型的值会在需要时自动转换。

JSON工具函数

以下函数在BPMN和CMMN上下文中处理JSON对象时可用:

表达式描述
${json:object()}返回一个空的JSON对象
${json:array()}返回一个空的JSON数组
${json:arrayWithSize(size)}返回指定大小的JSON数组
${json:addToArray(arrayNode, object)}将给定对象添加到给定的数组节点中

格式化工具函数

以下方法用于格式化值:

表达式描述
${flwFormatUtils.formatString(text, substitutes)}根据Java格式化器规范格式化字符串
${flwFormatUtils.formatDate(value, dateFormat)}使用给定格式将值格式化为字符串。支持Date、Instant、LocalDate、LocalDateTime或ISO8601格式的字符串
${flwFormatUtils.formatDecimal(pattern, decimal)}使用默认区域设置的十进制格式化器根据Java格式化器规范格式化字符串
${flwFormatUtils.formatStringWithLocale(languageTag, text, substitutes)}根据Java格式化器规范格式化字符串。字符串根据提供的语言标签的区域设置格式进行格式化
${flwFormatUtils.formatCurrencyWithLocale(currencyCode, amount, languageTag)}根据提供的语言标签的区域设置格式化货币金额

示例:${flwFormatUtils.formatDate(flwTimeUtils.now(), 'dd.MM.yyyy')} 将今天的日期格式化为dd.MM.yyyy格式

区域设置工具函数

表达式描述
${flwLocaleUtils.getLocaleForLanguageTag(languageTag)}返回具有给定语言标签的区域设置,例如'ch-DE'
${flwLocaleUtils.getAvailableLocales()}返回可用区域设置列表
${flwLocaleUtils.getDefaultLocale()}返回系统默认区域设置
${flwLocaleUtils.getAllCountryCodes()}返回所有2字母ISO国家代码列表
${flwLocaleUtils.getAllLanguageCodes()}返回所有ISO语言代码列表,例如"de"(不是"de-CH")
${flwLocaleUtils.getLanguageDisplayName(languageIsoCode, displayLanguageTag)}返回某种语言的单一语言名称,例如"German"或"Spanish"
${flwLocaleUtils.getCountryDisplayName(languageTag, displayLanguageTag)}返回某种语言的单一国家名称,例如"Switzerland"或"Germany"
${flwLocaleUtils.getAllLanguageDisplayNames(displayLanguageTag)}返回某种语言的所有语言名称列表
${flwLocaleUtils.getAllCountryDisplayNames(displayLanguageTag)}返回某种语言的所有国家名称列表

数学工具函数

以下方法用于执行数学运算:

表达式描述
${flwMathUtils.abs(number)}返回数字的绝对值
${flwMathUtils.average(numbers)}计算数字列表的平均值
${flwMathUtils.ceil(number)}返回提供的数字的下一个更高整数
${flwMathUtils.floor(number)}返回提供的数字的下一个更低整数
${flwMathUtils.median(numbers)}返回数字列表的中位数
${flwMathUtils.min(numbers)}返回数字列表中的最小数
${flwMathUtils.max(numbers)}返回数字列表中的最大数
${flwMathUtils.parseDouble(string)}将字符串转换为双精度值
${flwMathUtils.parseInt(string)}将字符串转换为整数值
${flwMathUtils.round(number)}将数字四舍五入为整数值
${flwMathUtils.round(number, scale)}使用RoundingMode#HALF_UP将数字四舍五入到最多小数位数
${flwMathUtils.sum(numbers)}计算数字列表的总和

用户函数

以下方法可用于检索用户信息:

表达式描述
${findUser(userId)}返回与传递的ID对应的用户表示
${findGroupMemberUserIds('GROUP_A, GROUP_B')}v3.14.0+ 返回给定组成员的userIds的字符串集合。注意:字符串集合String应该只是暂时存储,如果要将其作为变量持久化,需要将其转换为JSON
${findGroupMemberEmails('GROUP_A, GROUP_B')}v3.14.0+ 返回给定组成员的电子邮件的字符串集合。忽略没有电子邮件的用户
${isUserInAllGroups(userId, groupKeys)}如果给定的userId在具有给定键的提供组中,则返回true
${isUserInAnyGroup(userId, groupKeys)}如果给定的userId在具有给定键的提供组中的任何一个中,则返回true
${isUserInNoGroup(userId, groupKeys)}如果给定的userId不在具有给定键的提供组中的任何一个中,则返回true
${userInfo:findUserInfo(property, findUser(authenticatedUserId))}返回当前用户的给定用户信息属性
${userInfo:findBooleanUserInfo(property, findUser(authenticatedUserId))}返回当前用户的给定布尔用户信息属性

任务服务

任务服务可以在表达式中用于以编程方式与任务交互:

表达式描述
${taskService.claim(taskId, userId)}由具有提供的用户ID的用户认领给定ID的任务
${taskService.unclaim(taskId)}取消认领已认领的任务
${taskService.complete(taskId, userId, variables)}完成给定的任务。userId和variables(String到Objects的Map)是可选的
${taskService.setDueDate(taskId, userId)}为任务设置截止日期
${taskService.setPriority(taskId, userId)}为任务设置优先级
${taskService.setAssignee(taskId, userId)}将给定的userId设置为任务的受理人
${taskService.setOwner(taskId, userId)}将给定的userId设置为任务的所有者
${taskService.addCandidateUser(taskId, userId)}将给定的userId添加为任务的候选用户
${taskService.addCandidateGroup(taskId, userId)}将给定的groupId添加为任务的候选组
${taskService.addUserIdentityLink(taskId, userId, identityLink)}向给定任务添加用户身份链接
${taskService.addGroupIdentityLink(taskId, groupId, identityLink)}向给定任务添加组身份链接

序列模型

在使用序列模型时,经常需要在BPMN或CMMN实例的活动名称中生成这些数字。以下函数可用于此目的:

表达式描述
${seq:nextNumber(<parameter>, sequenceDefinitionKey)}返回序列的下一个数字。参数应该是具有对当前变量访问权限的运行时对象,如流程实例、案例实例、执行、任务、计划项实例等。sequenceDefinitionKey应与模型中设置的键匹配
${seq:next(<parameter>, sequenceDefinitionKey)}与前一个类似,但也考虑了在模型中设置的格式配置设置

配置服务

配置服务允许访问配置属性(例如在application.properties文件或类似文件中设置的属性)。在表达式中,此服务很有用,因为它允许根据环境特定属性(例如DEV、TEST、PROD环境)更改行为。

表达式描述
${propertyConfigurationService.getProperty(key, defaultValue)}返回配置属性的字符串值
${propertyConfigurationService.getBooleanProperty(key, defaultValue)}返回配置属性的布尔值
${propertyConfigurationService.getIntegerProperty(key, defaultValue)}返回配置属性的整数值

认证令牌

这个高级表达式在使用外部服务和OAuth时很有用。显然,在表达式中硬编码凭据是不可取的。此表达式将帮助解决这个问题:

${flwAuthTokenUtils.getAccessToken('externalServiceId')} - 注意:当给定的externalServiceId有可用的Spring Security OAuth2客户端配置时,解析该外部服务ID的访问令牌。

集合工具

以下选项用于处理集合或ContentItem列表:

表达式描述
${flwCollectionUtils.emptyNode()}创建一个空的JSON对象节点
${flwCollectionUtils.emptyList()}创建一个空的Java List。结果不应直接存储为非瞬态变量
${flwCollectionUtils.emptyMap()}创建一个空的Java Map。结果不应直接存储为非瞬态变量
${flwCollectionUtils.distinct(list)}从Java列表中删除重复元素。结果只应为ContentItems存储为非瞬态变量
${flwCollectionUtils.listWithElements(itemOne, itemTwo, ...)}创建包含多个元素的Java列表。结果只应为ContentItem存储为非瞬态变量
${flwCollectionUtils.mergeCollections(collection1, collection2, ...)}将多个Java集合合并为单个列表。结果只应为ContentItem存储为非瞬态变量

注意:当使用Java的List或Map时,它们将作为可序列化对象存储在Flowable数据库中。这意味着它们将存储在字节数组表中,对于大量数据可能会变得效率低下。ContentItem列表是唯一的例外。

表达式自定义

使用场景

让我们考虑一个实际的例子:假设有一个依赖于公司部门的流程实例。用户任务的分配取决于流程实例在哪个部门执行。表达式是确定受理人的自然方式。

假设有一个作为流程实例变量的部门引用。给定部门ID后,可以用它来查找部门成员,并使用该信息来确定给定流程的受理人。

使用Bean实现

扩展表达式功能最直接的方式是暴露一个带有方法的Spring Bean。默认情况下,表达式可以包含Spring Bean引用。

在Spring配置中的Bean定义:

@Bean
public DepartmentResolver departmentResolver() {
    return new DepartmentResolver();
}

这是一个简单的解析器示例:

public class DepartmentResolver {
    public String getAssignee(ExecutionEntity execution) {
        String departmentId = execution.getVariable("departmentId", String.class);
        return departmentId + ".assignee";
    }
}

最后一步是在流程定义中使用这个Bean:

<process id="oneTaskProcess">
  <startEvent id="start"/>
  <sequenceFlow id="flow1" sourceRef="start" targetRef="theTask"/>
  <userTask id="theTask" name="My Task" flowable:assignee="${departmentResolver.getAssignee(execution)}"/>
  <sequenceFlow id="flow2" sourceRef="theTask" targetRef="theEnd" />
  <endEvent id="theEnd"/>
</process>

下面的JUnit测试验证了用户任务受理人是否来自DepartmentResolver:

@Test
@Deployment(resources = "AssigneeExpressionBeanTest.oneTaskProcessWithBeanAssignment.bpmn20.xml")
public void springBeanAssignUserTask() {
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneTaskProcess", 
        Collections.singletonMap("departmentId", "testDepartmentId"));
    Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
    assertThat(task.getAssignee()).isEqualTo("testDepartmentId.assignee");
}

对于CMMN示例,请参考SpringCustomBeanTest及其相关文件和资源。

使用表达式解析器实现

使用表达式Bean(如${departmentResolver.getAssignee(execution)})非常容易,但它们不是一等公民表达式。它们需要执行方法调用并将execution作为参数传入方法。为了避免这一步,我们可以使用简单的受理人表达式格式${departmentAssignee}。这需要扩展引擎表达式管理器中的表达式解析器(ELResolvers)。

@Bean
public SpringProcessEngineConfiguration processEngineConfiguration(ApplicationContext applicationContext, 
    DataSource dataSource, PlatformTransactionManager transactionManager) {
    SpringProcessEngineConfiguration configuration = new SpringProcessEngineConfiguration();
    configuration.setDataSource(dataSource);
    configuration.setTransactionManager(transactionManager);
    configuration.setDatabaseSchemaUpdate("true");
    configuration.setExpressionManager(new CustomExpressionManager(applicationContext, Collections.emptyMap()));
    return configuration;
}

CustomExpressionManager通过新的部门特定ELResolver扩展了可用的ELResolvers集合:

public static class CustomExpressionManager extends SpringExpressionManager {
    @Override
    protected ELResolver createElResolver(VariableContainer variableContainer) {
        CompositeELResolver compositeElResolver = new CompositeELResolver();
        compositeElResolver.add(createVariableElResolver(variableContainer));
        compositeElResolver.add(createSpringElResolver());
        compositeElResolver.add(new ArrayELResolver());
        compositeElResolver.add(new ListELResolver());
        compositeElResolver.add(new MapELResolver());
        compositeElResolver.add(new JsonNodeELResolver());
        compositeElResolver.add(new BeanELResolver());
        compositeElResolver.add(new DepartmentELResolver(variableContainer));
        compositeElResolver.add(new CouldNotResolvePropertyELResolver());
        return compositeElResolver;
    }
}

DepartmentELResolver解析"departmentAssignee"关键字并返回其关联的部门特定值:

public static class DepartmentELResolver extends VariableContainerELResolver {
    public DepartmentELResolver(VariableContainer variableContainer) {
        super(variableContainer);
    }

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (base == null && "departmentAssignee".equals(property) && variableContainer instanceof ExecutionEntity) {
            Object departmentId = variableContainer.getVariable("departmentId");
            if (departmentId == null) {
                throw new RuntimeException("departmentId was not found in execution " + 
                    ((ExecutionEntity) variableContainer).getId());
            }
            context.setPropertyResolved(true);
            return getDepartmentAssignee(departmentId);
        }
        return null;
    }

    protected String getDepartmentAssignee(Object departmentId) {
        return departmentId + ".assignee";
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        return true;
    }
}

现在流程模型用户任务规范变为:

<userTask id="theTask" name="My Task" flowable:assignee="${departmentAssignee}"/>

设置值

到目前为止,所有表达式示例都只是返回值。但是,表达式也可以用来改变值。例如,给定这个人工任务案例模型:

<case id="oneHumanTaskCase">
    <casePlanModel id="myPlanModel" name="My CasePlanModel">
        <planItem id="planItem1" name="The Task" definitionRef="theTask" />
        <humanTask id="theTask" name="The Task" isBlocking="true" flowable:assignee="johnDoe"/>
    </casePlanModel>
</case>

可以在不使用任何表达式的情况下更新案例实例变量,如下面的JUnit测试所示:

@Test
@CmmnDeployment(resources = "org/flowable/cmmn/test/task/CmmnTaskServiceTest.testOneHumanTaskCase.cmmn")
public void testOneHumanTaskCaseScopeExpression() {
    // 创建带有一个人工任务的案例实例
    CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
                    .caseDefinitionKey("oneHumanTaskCase")
                    // 创建变量并设置其值。默认表达式管理器允许变量更新
                    .variable("variableToUpdate", "VariableValue")
                    .start();
    Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();

    // 完成任务并更新案例实例中的变量
    cmmnTaskService.complete(task.getId(), Collections.singletonMap(
            "${variableToUpdate}", "updatedVariableValue"
        )
    );
    HistoricCaseInstance historicCaseInstance = cmmnHistoryService
                    .createHistoricCaseInstanceQuery()
                    .caseInstanceId(caseInstance.getId())
                    .includeCaseVariables().singleResult();
    assertThat(historicCaseInstance.getCaseVariables().get("variableToUpdate"), is("updatedVariableValue"));
}

代码将变量variableToUpdate添加到案例中,值为"VariableValue"。在任务完成时,VariableContainerELResolver解析表达式${variableToUpdate}并为变量设置新值"updatedVariableValue"。assertThat()验证这确实发生了。

更多设置值的表达式示例可以在CmmnTaskServiceTest中找到。

Flowable中的四种核心流程控制模式:会签、或签、分支与并行

March 27, 2025

在业务流程管理(BPM)领域,合理选择和应用流程模式对于满足复杂业务需求至关重要。Flowable作为一个灵活的BPM引擎,支持多种流程控制模式的实现。本文将重点介绍四种常用的流程模式:会签流程、或签流程、分支流程和并行流程,并结合Flowable展示其具体实现方法。

流程模式基础概念

在深入了解各种流程模式之前,先简单介绍一下BPMN基本概念:

  • 流程定义(Process Definition):业务流程的静态模型
  • 流程实例(Process Instance):流程定义的运行时执行实例
  • 任务(Task):流程中的工作单元,通常需要人工处理
  • 网关(Gateway):控制流程执行路径的节点
  • 流程变量(Process Variable):存储和传递流程数据的容器

会签流程模式

会签流程(也称为"全部审批"模式)要求多个参与者必须全部同意才能继续流程。这常用于重要决策审批、合同审核等场景。

会签流程模式

会签流程的特点

  • 多人必须全部完成任务才能继续流程
  • 可设置驳回机制,任一人拒绝即可终止流程
  • 可配置顺序执行或并行执行

Flowable中的会签实现

在Flowable中,会签流程主要通过多实例用户任务(Multi-Instance User Task)实现,配合条件表达式控制流程走向。

<userTask id="approvalTask" name="部门经理审批" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false">
    <loopCardinality>${nrOfApprovers}</loopCardinality>
    <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

上面的示例中:

  • isSequential="false" 表示所有审批任务会并行创建
  • completionCondition 设置为只有当所有实例都完成时才继续流程

会签流程的核心API使用

// 设置审批人列表
List<String> approverList = Arrays.asList("manager1", "manager2", "manager3");
Map<String, Object> variables = new HashMap<>();
variables.put("approverList", approverList);
variables.put("nrOfApprovers", approverList.size());

// 启动会签流程
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
    "contractApprovalProcess", 
    "CONTRACT-2025-001", 
    variables
);

// 查询当前待办任务
List<Task> tasks = taskService.createTaskQuery()
    .processInstanceId(processInstance.getId())
    .taskCandidateOrAssigned("manager1")
    .list();

会签流程的高级应用

在实际应用中,会签流程常常需要处理一些特殊情况:

  1. 动态会签人:根据业务数据动态确定审批人

    // 动态构建审批人列表
    List<String> approvers = userService.getDepartmentManagers(departmentId);
    execution.setVariable("approverList", approvers);
    execution.setVariable("nrOfApprovers", approvers.size());
    
  2. 会签结果汇总:收集所有审批人的意见

    <multiInstanceLoopCharacteristics isSequential="false">
      <loopCardinality>${nrOfApprovers}</loopCardinality>
      <inputDataItem name="approver" />
      <inputDataItem name="comments" />
      <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
    </multiInstanceLoopCharacteristics>
    

或签流程模式

或签流程(也称为"任一审批"模式)只需要多个参与者中的任意一个同意即可继续流程。这适用于一般性审批、资源分配等场景。

或签流程模式

或签流程的特点

  • 任意一人完成任务即可继续流程
  • 适合平级审批或并行审批场景
  • 提高审批效率,避免流程阻塞

Flowable中的或签实现

或签流程在Flowable中同样使用多实例用户任务实现,但通过完成条件(completionCondition)控制只需一人完成即可。

<userTask id="approvalTask" name="部门审批" flowable:assignee="${assignee}">
  <multiInstanceLoopCharacteristics isSequential="false">
    <loopCardinality>${nrOfApprovers}</loopCardinality>
    <completionCondition>${nrOfCompletedInstances >= 1}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

上面的示例中:

  • completionCondition 设置为只要有一个实例完成即可继续流程

候选人方式实现或签

除了多实例方式外,也可以使用候选人(Candidate)机制实现或签:

<userTask id="approvalTask" name="部门审批">
  <potentialOwner>
    <resourceAssignmentExpression>
      <formalExpression>user1,user2,user3</formalExpression>
    </resourceAssignmentExpression>
  </potentialOwner>
</userTask>

配合Java代码:

// 设置候选人
List<String> candidateUsers = Arrays.asList("user1", "user2", "user3");
for (String user : candidateUsers) {
    taskService.addCandidateUser(task.getId(), user);
}

// 认领任务
taskService.claim(taskId, "user1");

// 完成任务
taskService.complete(taskId);

分支流程模式

分支流程(也称为"条件路由"模式)根据业务条件选择不同的执行路径。这是BPM中最常用的控制流模式之一。

分支流程模式

分支流程的特点

  • 基于条件表达式动态选择流程路径
  • 可实现复杂的业务决策逻辑
  • 支持默认路径设置

排他网关实现分支

在Flowable中,分支流程主要通过排他网关(Exclusive Gateway)实现:

<exclusiveGateway id="approvalDecision" name="审批决策" />

<sequenceFlow id="flow1" sourceRef="approvalDecision" targetRef="approvedPath">
  <conditionExpression xsi:type="tFormalExpression">${approved == true}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow2" sourceRef="approvalDecision" targetRef="rejectedPath">
  <conditionExpression xsi:type="tFormalExpression">${approved == false}</conditionExpression>
</sequenceFlow>

包容网关实现复杂分支

当需要同时选择多个满足条件的分支时,可以使用包容网关(Inclusive Gateway):

<inclusiveGateway id="riskAssessment" name="风险评估" />

<sequenceFlow id="flow1" sourceRef="riskAssessment" targetRef="creditCheck">
  <conditionExpression xsi:type="tFormalExpression">${amount > 10000}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow2" sourceRef="riskAssessment" targetRef="identityVerification">
  <conditionExpression xsi:type="tFormalExpression">${customerType == 'new'}</conditionExpression>
</sequenceFlow>

<sequenceFlow id="flow3" sourceRef="riskAssessment" targetRef="basicProcessing">
  <conditionExpression xsi:type="tFormalExpression">${amount <= 10000 && customerType != 'new'}</conditionExpression>
</sequenceFlow>

动态分支控制

在实际应用中,我们可以通过Java代码动态设置流程变量,进而控制分支流向:

// 设置决策变量
Map<String, Object> variables = new HashMap<>();
variables.put("approved", riskScore > 70);
variables.put("amount", loanRequest.getAmount());
variables.put("customerType", customer.getType());

// 完成任务,触发网关决策
taskService.complete(taskId, variables);

并行流程模式

并行流程(也称为"同步执行"模式)允许多个活动同时执行,提高流程效率。适用于相互独立的任务处理场景。

并行流程模式

并行流程的特点

  • 多个任务可同时执行,互不干扰
  • 可设置同步点,等待所有分支完成
  • 提高流程执行效率,缩短总处理时间

并行网关实现

在Flowable中,并行流程通过并行网关(Parallel Gateway)实现:

<!-- 分叉:开始并行执行 -->
<parallelGateway id="forkProcess" />
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="forkProcess" />

<sequenceFlow id="flow2" sourceRef="forkProcess" targetRef="documentReview" />
<sequenceFlow id="flow3" sourceRef="forkProcess" targetRef="technicalReview" />
<sequenceFlow id="flow4" sourceRef="forkProcess" targetRef="financialReview" />

<!-- 各并行任务 -->
<userTask id="documentReview" name="文档审查" />
<userTask id="technicalReview" name="技术评估" />
<userTask id="financialReview" name="财务审核" />

<!-- 汇合:等待所有并行分支完成 -->
<sequenceFlow id="flow5" sourceRef="documentReview" targetRef="joinProcess" />
<sequenceFlow id="flow6" sourceRef="technicalReview" targetRef="joinProcess" />
<sequenceFlow id="flow7" sourceRef="financialReview" targetRef="joinProcess" />
<parallelGateway id="joinProcess" />

<sequenceFlow id="flow8" sourceRef="joinProcess" targetRef="finalApproval" />

并行流与会签结合

并行流和会签可以结合使用,实现更复杂的业务场景:

<parallelGateway id="forkProcess" />

<sequenceFlow id="flow1" sourceRef="forkProcess" targetRef="hrApproval" />
<sequenceFlow id="flow2" sourceRef="forkProcess" targetRef="financeApproval" />

<!-- HR部门会签 -->
<userTask id="hrApproval" name="HR部门审批" flowable:assignee="${hrApprover}">
  <multiInstanceLoopCharacteristics isSequential="false">
    <loopCardinality>${hrApproverCount}</loopCardinality>
    <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

<!-- 财务部门会签 -->
<userTask id="financeApproval" name="财务部门审批" flowable:assignee="${financeApprover}">
  <multiInstanceLoopCharacteristics isSequential="false">
    <loopCardinality>${financeApproverCount}</loopCardinality>
    <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
  </multiInstanceLoopCharacteristics>
</userTask>

<parallelGateway id="joinProcess" />

实际应用场景:采购审批流程

下面是一个结合了上述四种流程模式的采购审批流程示例:

采购审批流程综合示例

<process id="purchaseApprovalProcess" name="采购审批流程">
  <startEvent id="startEvent" />
  <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="initiatePurchase" />
  
  <!-- 初始化采购申请 -->
  <userTask id="initiatePurchase" name="填写采购申请" />
  <sequenceFlow id="flow2" sourceRef="initiatePurchase" targetRef="amountGateway" />
  
  <!-- 分支流:根据金额决定审批路径 -->
  <exclusiveGateway id="amountGateway" />
  <sequenceFlow id="flow3" sourceRef="amountGateway" targetRef="simpleApproval">
    <conditionExpression xsi:type="tFormalExpression">${amount <= 10000}</conditionExpression>
  </sequenceFlow>
  <sequenceFlow id="flow4" sourceRef="amountGateway" targetRef="complexApprovalFork">
    <conditionExpression xsi:type="tFormalExpression">${amount > 10000}</conditionExpression>
  </sequenceFlow>
  
  <!-- 或签流程:小额采购任一经理审批 -->
  <userTask id="simpleApproval" name="经理审批">
    <potentialOwner>
      <resourceAssignmentExpression>
        <formalExpression>manager1,manager2,manager3</formalExpression>
      </resourceAssignmentExpression>
    </potentialOwner>
  </userTask>
  <sequenceFlow id="flow5" sourceRef="simpleApproval" targetRef="endEvent" />
  
  <!-- 并行流程:大额采购需多部门审核 -->
  <parallelGateway id="complexApprovalFork" />
  <sequenceFlow id="flow6" sourceRef="complexApprovalFork" targetRef="departmentApproval" />
  <sequenceFlow id="flow7" sourceRef="complexApprovalFork" targetRef="financeCheck" />
  
  <!-- 会签流程:部门经理全部审批 -->
  <userTask id="departmentApproval" name="部门经理审批" flowable:assignee="${assignee}">
    <multiInstanceLoopCharacteristics isSequential="false">
      <loopCardinality>${nrOfManagers}</loopCardinality>
      <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
    </multiInstanceLoopCharacteristics>
  </userTask>
  
  <!-- 财务审核 -->
  <userTask id="financeCheck" name="财务审核" />
  
  <!-- 汇合所有审批 -->
  <sequenceFlow id="flow8" sourceRef="departmentApproval" targetRef="complexApprovalJoin" />
  <sequenceFlow id="flow9" sourceRef="financeCheck" targetRef="complexApprovalJoin" />
  <parallelGateway id="complexApprovalJoin" />
  
  <!-- 最终CEO审批 -->
  <sequenceFlow id="flow10" sourceRef="complexApprovalJoin" targetRef="ceoApproval" />
  <userTask id="ceoApproval" name="CEO审批" />
  <sequenceFlow id="flow11" sourceRef="ceoApproval" targetRef="endEvent" />
  
  <endEvent id="endEvent" />
</process>

使用Java代码启动上述流程:

// 设置流程变量
Map<String, Object> variables = new HashMap<>();
variables.put("amount", 15000);
variables.put("nrOfManagers", 3);
variables.put("assignee1", "manager1");
variables.put("assignee2", "manager2");
variables.put("assignee3", "manager3");

// 启动流程实例
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
    "purchaseApprovalProcess", 
    "PUR-2025-001", 
    variables
);

流程模式设计最佳实践

在使用Flowable设计和实现上述流程模式时,以下是一些最佳实践建议:

  1. 明确业务需求:在选择流程模式前,明确业务需要"会签"还是"或签",是否有分支条件等。

  2. 简化流程设计:流程设计应尽量简洁,避免过度复杂的路由逻辑。

  3. 合理使用表达式:流程条件表达式应清晰、简单,易于维护。

  4. 考虑异常处理:设计流程时,需考虑异常情况,如审批人不在岗、驳回处理等。

  5. 预留扩展点:流程设计应考虑未来可能的变更,预留扩展点。

  6. 使用监听器记录关键事件:

    public class ApprovalTaskListener implements TaskListener {
        @Override
        public void notify(DelegateTask delegateTask) {
            if (TaskListener.EVENTNAME_COMPLETE.equals(delegateTask.getEventName())) {
                // 记录任务完成事件
                boolean approved = (boolean) delegateTask.getVariable("approved");
                String comment = (String) delegateTask.getVariable("comment");
                LoggingService.logApproval(delegateTask.getId(), approved, comment);
            }
        }
    }
    

总结

BPM流程设计中的会签流程、或签流程、分支流程和并行流程是解决复杂业务场景的基础模式。Flowable通过其灵活的BPMN实现和丰富的API,提供了这些模式的完整支持。通过合理组合这些模式,可以构建出满足各种业务需求的复杂工作流,提高业务流程的自动化水平和执行效率。

在实际应用中,应根据具体业务场景选择适当的流程模式,并遵循流程设计的最佳实践,确保流程定义清晰、高效且可维护。Flowable作为一个成熟的BPM引擎,为这些流程模式的落地实施提供了强大的技术支持。

Flowable 案例迁移

March 17, 2025

在业务流程管理系统中,案例定义(Case Definition)的版本更新是一个常见的需求。Flowable提供了强大的案例迁移(Case Migration)功能,允许您将案例实例从一个定义版本迁移到另一个版本。本文将详细介绍这个功能的使用方法。

迁移API概览

Flowable提供了多个层级的迁移API:

  1. 单个案例实例迁移
  2. 从特定案例定义迁移所有案例实例
  3. 批量迁移特定案例定义的所有案例实例

除了上述API外,Flowable还支持历史案例实例的迁移,这对于使用案例重新激活功能特别有用。需要注意的是,在迁移历史案例实例时,不能指定迁移映射和表达式,因为案例仍然处于关闭状态。

迁移文档简单示例

迁移过程的核心是迁移文档(Migration Document),这是一个JSON格式的配置文件。让我们通过一个具体的例子来说明:

假设我们要将以下testMigrationCase案例(包含两个人工任务,通过sentry连接):

CMMN案例迁移前

迁移到新版本(包含一个人工任务和一个服务任务,通过sentry连接):

CMMN案例迁移后

迁移文档的JSON结构如下:

{
  "toCaseDefinitionKey": "testMigrationCase",
  "toCaseDefinitionVersion": 2,
  "terminatePlanItemDefinitions": [
    {
      "planItemDefinitionId": "humanTask2"
    }
  ],
  "moveToAvailablePlanItemDefinitions": [
    {
      "planItemDefinitionId": "serviceTask1"
    }
  ]
}

迁移文档的主要配置项

1. 目标定义配置

  • toCaseDefinitionId:目标案例定义的唯一ID
  • toCaseDefinitionKey:目标案例定义的键值
  • toCaseDefinitionVersion:目标案例定义的版本号
  • toCaseDefinitionTenantId:多租户环境下的租户ID

2. 状态变更配置

迁移文档支持多种状态变更操作:

  1. 激活计划项(Activate Plan Items)

    • 用于激活新的计划项
    • 可设置新负责人和本地变量
  2. 移动到可用状态(Move to Available)

    • 适用于由sentry控制的新计划项
    • 可设置本地变量
  3. 终止计划项(Terminate Plan Items)

    • 用于移除或停止计划项
    • 仅对活动项生效
  4. 重复等待状态管理

    • waitingForRepetitionPlanItemDefinitions:添加重复等待
    • removeWaitingForRepetitionPlanItemDefinitions:移除重复等待

3. 高级配置

  • 预升级和后升级表达式:用于执行迁移前后的自定义逻辑
  • 案例实例变量:设置案例级别的变量
  • 计划项ID变更:处理计划项ID发生变化的情况

CMMN迁移服务接口

Flowable提供了CmmnMigrationService接口来管理案例实例的迁移。以下是主要的接口方法:

迁移构建器创建方法

// 创建案例实例迁移构建器
CaseInstanceMigrationBuilder createCaseInstanceMigrationBuilder();

// 从迁移文档创建案例实例迁移构建器
CaseInstanceMigrationBuilder createCaseInstanceMigrationBuilderFromCaseInstanceMigrationDocument(CaseInstanceMigrationDocument document);

// 创建历史案例实例迁移构建器
HistoricCaseInstanceMigrationBuilder createHistoricCaseInstanceMigrationBuilder();

// 从迁移文档创建历史案例实例迁移构建器
HistoricCaseInstanceMigrationBuilder createHistoricCaseInstanceMigrationBuilderFromHistoricCaseInstanceMigrationDocument(HistoricCaseInstanceMigrationDocument document);

迁移验证方法

// 验证单个案例实例的迁移
CaseInstanceMigrationValidationResult validateMigrationForCaseInstance(String caseInstanceId, CaseInstanceMigrationDocument document);

// 验证案例定义下所有案例实例的迁移
CaseInstanceMigrationValidationResult validateMigrationForCaseInstancesOfCaseDefinition(String caseDefinitionId, CaseInstanceMigrationDocument document);

// 验证指定版本案例定义下所有案例实例的迁移
CaseInstanceMigrationValidationResult validateMigrationForCaseInstancesOfCaseDefinition(String caseDefinitionKey, int caseDefinitionVersion, String caseDefinitionTenantId, CaseInstanceMigrationDocument document);

迁移执行方法

// 迁移单个案例实例
void migrateCaseInstance(String caseInstanceId, CaseInstanceMigrationDocument document);

// 迁移单个历史案例实例
void migrateHistoricCaseInstance(String caseInstanceId, HistoricCaseInstanceMigrationDocument document);

// 迁移案例定义下的所有案例实例
void migrateCaseInstancesOfCaseDefinition(String caseDefinitionId, CaseInstanceMigrationDocument document);

// 迁移案例定义下的所有历史案例实例
void migrateHistoricCaseInstancesOfCaseDefinition(String caseDefinitionId, HistoricCaseInstanceMigrationDocument document);

// 迁移指定版本案例定义下的所有案例实例
void migrateCaseInstancesOfCaseDefinition(String caseDefinitionKey, int caseDefinitionVersion, String caseDefinitionTenantId, CaseInstanceMigrationDocument document);

// 迁移指定版本案例定义下的所有历史案例实例
void migrateHistoricCaseInstancesOfCaseDefinition(String caseDefinitionKey, int caseDefinitionVersion, String caseDefinitionTenantId, HistoricCaseInstanceMigrationDocument document);

批量迁移方法

// 批量迁移案例定义下的所有案例实例
Batch batchMigrateCaseInstancesOfCaseDefinition(String caseDefinitionId, CaseInstanceMigrationDocument document);

// 批量迁移案例定义下的所有历史案例实例
Batch batchMigrateHistoricCaseInstancesOfCaseDefinition(String caseDefinitionId, HistoricCaseInstanceMigrationDocument document);

// 批量迁移指定版本案例定义下的所有案例实例
Batch batchMigrateCaseInstancesOfCaseDefinition(String caseDefinitionKey, int caseDefinitionVersion, String caseDefinitionTenantId, CaseInstanceMigrationDocument document);

// 批量迁移指定版本案例定义下的所有历史案例实例
Batch batchMigrateHistoricCaseInstancesOfCaseDefinition(String caseDefinitionKey, int caseDefinitionVersion, String caseDefinitionTenantId, HistoricCaseInstanceMigrationDocument document);

// 获取批量迁移的结果
CaseInstanceBatchMigrationResult getResultsOfBatchCaseInstanceMigration(String migrationBatchId);

实际应用场景

假设您有一个"项目案例"的CMMN模型,当前版本包含以下阶段和计划项:

  1. 项目准备阶段

    • 项目章程创建(人工任务)
    • 项目团队组建(人工任务)
    • 项目计划制定(人工任务)
  2. 项目执行阶段

    • 需求分析(人工任务)
    • 设计开发(人工任务)
    • 测试验证(人工任务)

现在需要升级到新版本,增加以下改进:

  1. 在项目准备阶段增加风险评估计划项
  2. 将项目计划制定改为自动服务任务
  3. 在项目执行阶段增加代码审查计划项

迁移文档示例:

{
  "toCaseDefinitionKey": "projectCase",
  "toCaseDefinitionVersion": 2,
  "terminatePlanItemDefinitions": [
    {
      "planItemDefinitionId": "projectPlanTask"
    }
  ],
  "moveToAvailablePlanItemDefinitions": [
    {
      "planItemDefinitionId": "riskAssessmentPlan",
      "localVariables": {
        "riskAssessmentType": "comprehensive"
      }
    },
    {
      "planItemDefinitionId": "codeReviewTask"
    }
  ],
  "activatePlanItemDefinitions": [
    {
      "planItemDefinitionId": "autoProjectPlan",
      "localVariables": {
        "planTemplate": "standard"
      }
    }
  ]
}

使用建议

  1. 在进行迁移前,建议先备份相关数据
  2. 仔细规划迁移策略,确保所有必要的状态转换都被正确处理
  3. 使用条件表达式(condition)来控制迁移行为
  4. 在测试环境中先进行迁移测试
  5. 记录迁移过程中的关键操作和结果

总结

Flowable的Case Migration功能提供了灵活且强大的案例版本迁移能力。通过合理使用迁移文档中的各种配置项,您可以安全地将案例实例从一个版本迁移到另一个版本,同时保持业务流程的连续性和完整性。

Flowable 自动部署模型

March 15, 2025

Flowable 提供了从类路径上可用的模型和配置文件进行自动部署的选项。这种功能极大地简化了开发和部署流程,使开发者能够专注于业务逻辑而非部署细节。 Flowable 自动部署概述

应用自动部署

使用自动部署功能最简单的方式是从类路径将应用部署到 Flowable 平台。通过这种方式,应用中包含的所有模型都将被部署并与同一父部署相连接。以下文件夹默认支持:

  • apps/
  • com/flowable/app/default/**/
  • com/flowable/app/custom/**/
  • com/flowable/app/example/**/

在这些文件夹中,只有后缀为 .bar 和 .zip 的应用文件会被考虑。可以通过在应用属性中设置 flowable.app.resource-location 值来更改应用文件夹。

主要模型自动部署

流程模型

  • 默认文件夹:processes/
  • 支持文件:.bpmn 和 .bpmn20.xml
  • 配置属性:flowable.process-definition-location-prefix

案例模型

  • 默认文件夹:cases/
  • 支持文件:.cmmn 和 .cmmn.xml
  • 配置属性:flowable.cmmn.resource-location

决策模型

  • 默认文件夹:dmn/
  • 支持文件:.dmn 和 .dmn.xml
  • 配置属性:flowable.dmn.resource-location

表单模型

  • 默认文件夹:forms/
  • 支持文件:.form 和 .page
  • 配置属性:flowable.form.resource-location

Flowable Work 默认模型

Flowable Work 应用包含多个默认模型,这些模型在应用启动时自动部署。主要包括:

默认动作模型

提供了一系列标准动作,如:

  • 取消流程实例
  • 完成用户任务
  • 开始进行任务
  • 取消案例实例
  • 用户管理相关动作

默认安全策略

包含两个重要的安全策略模型:

  • 常规安全策略(basic-security-policy):用于所有权限检查
  • 严格安全策略:提供更严格的权限控制

默认内容模型

提供三种基本内容类型:

  • 合同
  • 通用
  • 策略

总结

Flowable 的自动部署功能为开发者提供了一种简单而强大的方式,从类路径直接部署各种模型和配置文件到 Flowable 平台。通过合理利用这些功能,可以显著提高开发效率和部署可靠性。

无论是使用默认的部署路径,还是通过配置属性自定义部署选项,Flowable 都提供了灵活的方式来满足各种部署需求。

Flowable-UI 入门指南:从零开始的 BPM 之旅

March 14, 2025

什么是Flowable-UI?

Flowable-UI 是 Flowable BPM 平台的用户界面组件,作为一个轻量级的开源工具,它提供了直观的图形化界面,让用户能够便捷地进行业务流程管理。无需编写代码,用户就能快速验证和实现业务流程逻辑,这使其成为流程原型开发的理想选择。

核心模块

Flowable-UI 整合了四个核心模块,每个模块都专注于特定的功能领域: Flowable-UI 模块架构

  1. Flowable Modeler(流程建模器)
    • 基于 BPMN 2.0 标准的流程设计工具
    • 支持拖拽式的流程图设计
    • 可视化的流程定义和编辑功能

Flowable Modeler 界面

  1. Flowable Task(任务管理)
    • 流程实例的启动和管理
    • 用户任务的处理和分配
    • 实时流程状态跟踪

Task 模块界面

  1. Flowable Admin(管理控制台)
    • 流程引擎的监控和管理
    • 提供数据查询功能
    • 流程调试工具

Admin 控制台

  1. Flowable IDM(身份管理)
    • 用户和角色管理
    • 权限控制
    • 支持单点登录
    • 多应用协同管理

IDM 管理界面

主要功能

通过 Flowable-UI,用户可以实现以下核心功能:

  • 直观地创建和编辑业务流程
  • 支持多种建模标准:
    • BPMN(业务流程建模与符号) BPMN 建模示例
    • CMMN(案例管理模型与符号) CMMN 建模示例
    • DMN(决策模型与符号) DMN 建模示例
  • 覆盖从流程设计到执行的完整生命周期管理

快速部署

Flowable-UI 提供了 Docker 镜像,可以通过以下简单步骤快速部署。

1. 使用默认 H2 数据库部署

最简单的部署方式是使用默认的 H2 内存数据库:

docker run -p 8080:8080 flowable/flowable-ui

2. 使用 MySQL 数据库部署

在生产环境中,建议使用 MySQL 等关系型数据库来持久化数据。以下是使用 MySQL 的部署步骤,完整的部署配置代码可以在 GitHub 仓库 中找到。

  1. 创建配置文件 flowable-default.properties,并修改下面的配置:
# 数据库配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://mysql:3306/flowable?characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
  1. 下载 MySQL 驱动:

    • 从 Maven 中央仓库下载 MySQL 驱动:mysql-connector-java-8.0.27.jar
  2. 创建 docker-compose.yml 文件:

version: '3'

services:
  flowable-ui:
    image: flowable/flowable-ui
    container_name: flowable-ui
    restart: always
    ports:
      - 8080:8080
    volumes:
      - ./flowable-default.properties:/app/WEB-INF/classes/flowable-default.properties
      - ./mysql-connector-java-8.0.27.jar:/app/WEB-INF/lib/mysql-connector-java-8.0.27.jar
  mysql:
    image: mysql:5.7
    environment:
      TZ: Asia/Shanghai
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: flowable
    restart: always
    volumes:
      - ./data/mysql/data:/var/lib/mysql
  1. 启动服务:
docker-compose up -d
  1. 访问应用 Flowable-UI 界面概览 部署完成后,访问以下地址:
  • 打开浏览器访问 http://localhost:8080/flowable-ui
  • 默认登录凭据:
    • 用户名:admin
    • 密码:test

适用场景

Flowable-UI 特别适合以下场景:

  • 需要快速验证业务流程逻辑的项目
  • 希望通过可视化方式设计流程的团队
  • 追求低代码/无代码流程开发的组织
  • 需要统一的流程管理平台的企业

通过 Flowable-UI,企业可以显著提升业务流程的设计和管理效率,实现流程自动化的快速落地。

重要提示

自 Flowable 7.0.0 版本起,Flowable-UI 应用程序(包括 Flowable Modeler、Task、Admin 和 IDM)已从开源项目中移除。本文内容适用于 Flowable 6.x 及更早版本的历史参考。

Flowable 数据库表结构详解

March 13, 2025

在Flowable工作流引擎中,数据库表结构是整个系统的基础。本文将详细介绍Flowable中最重要的几类表结构,帮助大家更好地理解Flowable的内部工作机制。

Flowable表结构概览

1. 通用命名规则

Flowable的表名都遵循特定的命名规则,主要包括以下前缀:

  • ACT_RE_*:'RE'表示repository,这些表包含流程定义和流程静态资源
  • ACT_RU_*:'RU'表示runtime,这些表存储流程运行时的数据
  • ACT_HI_*:'HI'表示history,这些表存储历史数据
  • ACT_ID_*:'ID'表示identity,这些表存储身份信息
  • ACT_GE_*:'GE'表示general,这些表存储通用数据
  • ACT_APP_*:'APP'表示application,这些表存储应用级别的信息
  • ACT_CMMN_*:'CMMN'表示Case Management,这些表存储案例管理相关数据
  • ACT_DMN_*:'DMN'表示Decision Management,这些表存储决策管理相关数据
  • FLW_*:'FLW'表示Flowable特定的扩展表

2. 核心功能模块表结构

2.1 流程定义相关表(Repository)

  • ACT_RE_DEPLOYMENT:流程部署表

    • 主要字段:
      • ID_:部署ID,主键
      • NAME_:部署名称
      • CATEGORY_:部署类别
      • DEPLOY_TIME_:部署时间
      • TENANT_ID_:租户ID,用于多租户
    • 使用场景:每次部署流程定义文件时会产生记录
  • ACT_RE_PROCDEF:流程定义表

    • 主要字段:
      • ID_:主键
      • KEY_:流程定义的标识
      • VERSION_:版本号
      • NAME_:流程定义名称
      • RESOURCE_NAME_:资源文件名称
      • DGRM_RESOURCE_NAME_:流程图资源名称
      • SUSPENSION_STATE_:暂停状态
    • 使用场景:存储流程定义的元数据,支持流程定义的版本控制

2.2 运行时数据表(Runtime)

  • ACT_RU_EXECUTION:运行时流程执行实例表

    • 主要字段:
      • ID_:执行实例ID
      • PROC_INST_ID_:流程实例ID
      • BUSINESS_KEY_:业务标识
      • PARENT_ID_:父执行流ID
      • PROC_DEF_ID_:流程定义ID
      • SUPER_EXEC_:父流程实例ID(用于子流程)
    • 常用查询示例:
      -- 查询某个业务关联的流程实例
      SELECT * FROM ACT_RU_EXECUTION 
      WHERE BUSINESS_KEY_ = '业务ID';
      
      -- 查询所有活动的子流程
      SELECT * FROM ACT_RU_EXECUTION 
      WHERE SUPER_EXEC_ IS NOT NULL;
      
  • ACT_RU_TASK:运行时任务表

    • 主要字段:
      • ID_:任务ID
      • NAME_:任务名称
      • ASSIGNEE_:受理人
      • CREATE_TIME_:创建时间
      • DUE_DATE_:到期时间
      • PRIORITY_:优先级
      • CATEGORY_:任务类别
    • 性能优化建议:
      • 建议为ASSIGNEE_字段创建索引
      • 对于到期时间的查询,建议增加DUE_DATE_的索引
  • ACT_RU_VARIABLE:运行时变量表

    • 主要字段:
      • ID_:变量ID
      • TYPE_:变量类型
      • NAME_:变量名称
      • EXECUTION_ID_:所属执行实例ID
      • PROC_INST_ID_:所属流程实例ID
      • TASK_ID_:所属任务ID
      • BYTEARRAY_ID_:字节数组ID(用于存储序列化的变量)
    • 变量类型说明:
      • string:字符串类型
      • integer:整数类型
      • boolean:布尔类型
      • date:日期类型
      • serializable:序列化对象

2.3 历史数据表(History)

  • ACT_HI_PROCINST:历史流程实例表

    • 主要字段:
      • ID_:流程实例ID
      • START_TIME_:开始时间
      • END_TIME_:结束时间
      • DURATION_:持续时间
      • START_USER_ID_:启动用户
      • SUPER_PROCESS_INSTANCE_ID_:父流程实例ID
    • 数据清理建议:
      -- 删除30天前的已完成流程
      DELETE FROM ACT_HI_PROCINST 
      WHERE END_TIME_ < DATE_SUB(NOW(), INTERVAL 30 DAY);
      
  • ACT_HI_TASKINST:历史任务实例表

    • 主要字段:
      • ID_:任务实例ID
      • PROC_DEF_ID_:流程定义ID
      • TASK_DEF_KEY_:任务定义的ID
      • START_TIME_:开始时间
      • END_TIME_:结束时间
      • ASSIGNEE_:受理人
    • 常用统计查询:
      -- 统计用户任务处理时长
      SELECT 
        ASSIGNEE_,
        AVG(TIMESTAMPDIFF(MINUTE, START_TIME_, END_TIME_)) as avg_duration
      FROM ACT_HI_TASKINST
      WHERE END_TIME_ IS NOT NULL
      GROUP BY ASSIGNEE_;
      

2.4 身份数据表(Identity)

  • ACT_ID_USER:用户表

    • 主要字段:
      • ID_:用户ID
      • FIRST_:名
      • LAST_:姓
      • EMAIL_:邮箱
      • PWD_:密码
    • 安全建议:
      • 密码字段建议使用加密存储
      • 建议使用外部认证系统集成
  • ACT_ID_GROUP:用户组表

    • 主要字段:
      • ID_:组ID
      • NAME_:组名称
      • TYPE_:组类型
    • 应用场景:
      • 部门管理
      • 角色管理
      • 权限分组

2.5 作业和事件相关表

  • ACT_RU_JOB:运行时作业表

    • 主要字段:
      • ID_:作业ID
      • TYPE_:作业类型
      • DUEDATE_:到期时间
      • REPEAT_:重复规则
    • 相关作业表:
      • ACT_RU_TIMER_JOB:定时作业表
      • ACT_RU_SUSPENDED_JOB:暂停的作业表
      • ACT_RU_DEADLETTER_JOB:无法执行的作业表
      • ACT_RU_EXTERNAL_JOB:外部作业表
      • ACT_RU_HISTORY_JOB:历史作业表
  • 事件相关表:

    • ACT_RU_EVENT_SUBSCR:事件订阅表
    • ACT_EVT_LOG:事件日志表
    • FLW_EVENT_DEFINITION:事件定义表
    • FLW_CHANNEL_DEFINITION:通道定义表

2.6 通用支持表

  • ACT_GE_BYTEARRAY:二进制数据存储表

    • 存储流程定义文件、表单文件等
    • 主要字段:
      • ID_:主键
      • NAME_:文件名
      • BYTES_:二进制内容
      • DEPLOYMENT_ID_:部署ID
  • ACT_GE_PROPERTY:属性表

    • 存储系统级别的属性信息
    • 主要字段:
      • NAME_:属性名
      • VALUE_:属性值
      • REV_:版本号

3. 扩展功能模块表结构

3.1 CMMN(案例管理)相关表

  • 部署相关表:

    • ACT_CMMN_DEPLOYMENT:CMMN部署表
    • ACT_CMMN_CASEDEF:案例定义表
  • 运行时表:

    • ACT_CMMN_RU_CASE_INST:运行时案例实例表
    • ACT_CMMN_RU_PLAN_ITEM_INST:计划项实例表
  • 历史表:

    • ACT_CMMN_HI_CASE_INST:历史案例实例表

3.2 DMN(决策管理)相关表

  • 部署相关表:

    • ACT_DMN_DEPLOYMENT:DMN部署表
    • ACT_DMN_DECISION:决策表定义表
  • 历史表:

    • ACT_DMN_HI_DECISION_EXECUTION:决策执行历史表

4. 表关系与性能优化

4.1 核心表关系

流程引擎核心表关系:
ACT_RE_DEPLOYMENT ←──┐
      ↓              │
ACT_RE_PROCDEF ←─┐   │
      ↓          │   │
ACT_RU_EXECUTION │   │
      ↓          │   │
ACT_RU_TASK      │   │
      ↓          │   │
ACT_HI_PROCINST ─┘   │
      ↓              │
ACT_HI_TASKINST ────┘

4.2 性能优化建议

  1. 索引优化:

    CREATE INDEX idx_task_assignee ON ACT_RU_TASK(ASSIGNEE_);
    CREATE INDEX idx_hi_proc_inst_end ON ACT_HI_PROCINST(END_TIME_);
    CREATE INDEX idx_ru_execution_proc ON ACT_RU_EXECUTION(PROC_DEF_ID_);
    
  2. 数据清理策略:

    • 按年月分表存储历史数据
    • 定期归档历史数据
    • 及时清理过期数据
  3. 查询优化:

    • 优先使用API而非直接SQL查询
    • 合理使用分页查询
    • 避免全表扫描

5. 最佳实践与应用场景

5.1 常见应用场景

5.1.1 请假审批流程示例

让我们通过一个具体的请假审批流程来看数据是如何在各个表中流转的:

请假流程数据流转示意图

  1. 流程定义部署阶段
-- ACT_RE_DEPLOYMENT 新增一条部署记录
INSERT INTO ACT_RE_DEPLOYMENT(ID_, NAME_, DEPLOY_TIME_) 
VALUES ('1', '请假流程-V1', '2025-03-13 10:00:00');

-- ACT_RE_PROCDEF 新增一条流程定义记录
INSERT INTO ACT_RE_PROCDEF(ID_, NAME_, KEY_, VERSION_, DEPLOYMENT_ID_)
VALUES ('leave:1:1', '请假申请', 'leave', 1, '1');
  1. 发起请假申请
-- ACT_RU_EXECUTION 新增流程实例
INSERT INTO ACT_RU_EXECUTION(ID_, PROC_INST_ID_, BUSINESS_KEY_, PROC_DEF_ID_)
VALUES ('2001', '2001', 'LEAVE-2025-001', 'leave:1:1');

-- ACT_RU_TASK 新增员工填写任务
INSERT INTO ACT_RU_TASK(ID_, EXECUTION_ID_, PROC_INST_ID_, TASK_DEF_KEY_, NAME_)
VALUES ('2002', '2001', '2001', 'fillForm', '填写请假申请');

-- ACT_RU_VARIABLE 保存表单数据
INSERT INTO ACT_RU_VARIABLE(ID_, EXECUTION_ID_, NAME_, TYPE_, TEXT_)
VALUES 
('2003', '2001', 'days', 'integer', '3'),
('2004', '2001', 'reason', 'string', '年假');
  1. 直接主管审批
-- ACT_RU_TASK 更新前一个任务状态,新增审批任务
UPDATE ACT_RU_TASK SET END_TIME_ = NOW() WHERE ID_ = '2002';
INSERT INTO ACT_RU_TASK(ID_, EXECUTION_ID_, PROC_INST_ID_, TASK_DEF_KEY_, NAME_, ASSIGNEE_)
VALUES ('2005', '2001', '2001', 'leaderApprove', '直接主管审批', 'leader1');

-- ACT_HI_TASKINST 记录任务历史
INSERT INTO ACT_HI_TASKINST(ID_, PROC_INST_ID_, TASK_DEF_KEY_, NAME_, START_TIME_, END_TIME_)
VALUES ('2002', '2001', 'fillForm', '填写请假申请', '2025-03-13 10:05:00', '2025-03-13 10:10:00');
  1. 人事审批
-- ACT_RU_TASK 更新主管审批任务,新增人事审批任务
UPDATE ACT_RU_TASK SET END_TIME_ = NOW() WHERE ID_ = '2005';
INSERT INTO ACT_RU_TASK(ID_, EXECUTION_ID_, PROC_INST_ID_, TASK_DEF_KEY_, NAME_, ASSIGNEE_)
VALUES ('2006', '2001', '2001', 'hrApprove', '人事审批', 'hr1');

-- ACT_HI_TASKINST 记录主管审批历史
INSERT INTO ACT_HI_TASKINST(ID_, PROC_INST_ID_, TASK_DEF_KEY_, NAME_, START_TIME_, END_TIME_)
VALUES ('2005', '2001', 'leaderApprove', '直接主管审批', '2025-03-13 10:10:00', '2025-03-13 11:00:00');
  1. 流程结束
-- ACT_RU_TASK 删除最后一个任务
DELETE FROM ACT_RU_TASK WHERE ID_ = '2006';

-- ACT_RU_EXECUTION 删除执行实例
DELETE FROM ACT_RU_EXECUTION WHERE PROC_INST_ID_ = '2001';

-- ACT_HI_PROCINST 更新流程实例历史
UPDATE ACT_HI_PROCINST 
SET END_TIME_ = NOW(), 
    DURATION_ = TIMESTAMPDIFF(SECOND, START_TIME_, NOW())
WHERE PROC_INST_ID_ = '2001';

-- ACT_HI_TASKINST 记录最后一个任务历史
INSERT INTO ACT_HI_TASKINST(ID_, PROC_INST_ID_, TASK_DEF_KEY_, NAME_, START_TIME_, END_TIME_)
VALUES ('2006', '2001', 'hrApprove', '人事审批', '2025-03-13 11:00:00', '2025-03-13 11:30:00');

数据流转说明:

  1. 部署阶段:

    • 流程定义文件存储在 ACT_GE_BYTEARRAY
    • 部署信息记录在 ACT_RE_DEPLOYMENT
    • 流程定义信息记录在 ACT_RE_PROCDEF
  2. 运行时阶段:

    • 流程实例数据在 ACT_RU_EXECUTION
    • 当前任务数据在 ACT_RU_TASK
    • 流程变量在 ACT_RU_VARIABLE
  3. 历史记录:

    • 流程实例历史在 ACT_HI_PROCINST
    • 任务历史在 ACT_HI_TASKINST
    • 变量历史在 ACT_HI_VARINST

5.2 多引擎集成建议

  1. 流程引擎与CMMN集成:

    • 使用统一的事务管理
    • 共享用户数据
    • 统一历史数据管理
  2. DMN决策表使用:

    • 在流程中调用决策表
    • 在案例中使用决策节点

5.3 最佳实践清单

  1. 数据管理:

    • 定期清理历史数据
    • 实施合理的归档策略
    • 监控表数据量增长
  2. 性能优化:

    • 合理使用索引
    • 优化查询方式
    • 定期维护数据库
  3. 安全建议:

    • 注意多租户数据隔离
    • 加密敏感信息
    • 控制数据访问权限

总结

了解Flowable的表结构设计对于以下方面很有帮助:

  • 理解Flowable的工作原理
  • 进行流程调试和问题排查
  • 优化系统性能
  • 实现复杂业务场景
  • 多引擎协同工作

希望本文能帮助大家更好地理解Flowable的数据库设计,为后续的开发和维护工作提供参考。

Spring Boot + Flowable 工作流开发教程:整合 BPMN 和 CMMN 实战

March 12, 2025

Flowable 作为一个强大的开源工作流引擎,不仅支持传统的 BPMN 工作流,还提供了 CMMN 案例管理的能力,能够同时应对结构化和非结构化的业务场景。

本文将通过一个实际的服务台(Helpdesk)系统示例,详细介绍如何使用 Spring Boot 集成 Flowable,实现 BPMN 工作流与 CMMN 案例管理的完美结合。通过这个示例,你将学习到:

  • 如何在 Spring Boot 项目中集成 Flowable
  • BPMN 和 CMMN 的最佳实践及应用场景
  • 工作流引擎的核心 API 使用方法
  • 服务台系统的完整实现流程

让我们开始这段整合之旅吧!

1. BPMN vs CMMN

在开始之前,让我们先了解 BPMN 和 CMMN 的区别:

1.1 BPMN(Business Process Model and Notation)

  • 适用于结构化的业务流程
  • 强调顺序流和控制流
  • 流程是预定义的,步骤相对固定
  • 典型应用:请假审批、报销流程等

1.2 CMMN(Case Management Model and Notation)

  • 适用于非结构化的业务场景
  • 强调灵活性和适应性
  • 任务可以动态启动和完成
  • 典型应用:客服工单、医疗诊断等

2. 项目环境

  • JDK 8+
  • Spring Boot 2.7.x
  • Flowable 6.8.0(包含 BPMN 和 CMMN 引擎)
  • Maven

3. 项目初始化

3.1 添加依赖

首先创建一个 Spring Boot 项目,在 pom.xml 中添加以下依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.flowable</groupId>
    <artifactId>flowable-springboot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
    </parent>

    <properties>
        <java.version>8</java.version>
        <flowable.version>6.8.0</flowable.version>
    </properties>

    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Flowable -->
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter-rest</artifactId>
            <version>${flowable.version}</version>
        </dependency>
        <dependency>
            <groupId>org.flowable</groupId>
            <artifactId>flowable-spring-boot-starter-basic</artifactId>
            <version>${flowable.version}</version>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.2 配置文件

在 src/main/resources/application.yml 中添加以下配置:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1
    username: sa
    password:
    driver-class-name: org.h2.Driver
  h2:
    console:
      enabled: true
      path: /h2-console

flowable:
  database-schema-update: true
  async-executor-activate: false
  bpmn:
    enabled: true
  cmmn:
    enabled: true

4. 流程与案例模型

4.1 BPMN 流程模型

Helpdesk BPMN 流程模型

在 src/main/resources/processes 目录下创建 helpdesk.bpmn20.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
             xmlns:flowable="http://flowable.org/bpmn"
             targetNamespace="http://flowable.org/examples">

    <process id="leaveProcess" name="Leave Process">
        <startEvent id="startEvent" name="Start"/>
        <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="submitTask"/>

        <userTask id="submitTask" name="Submit Leave Request"
                  flowable:assignee="${applicant}"/>
        <sequenceFlow id="flow2" sourceRef="submitTask" targetRef="approveTask"/>

        <userTask id="approveTask" name="Approve Leave Request"
                  flowable:candidateGroups="managers"/>
        <sequenceFlow id="flow3" sourceRef="approveTask" targetRef="endEvent"/>

        <endEvent id="endEvent" name="End"/>
    </process>

</definitions> 

4.2 CMMN 案例模型

Helpdesk CMMN 案例模型

在 src/main/resources/cases 目录下创建 helpdesk-case.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"
             targetNamespace="http://www.flowable.org/casedef"
             exporter="Flowable Open Source Modeler"
             exporterVersion="6.8.0">

    <case id="helpDeskCase" name="Help Desk Case" flowable:initiatorVariableName="initiator">
        <documentation>Help Desk Case for handling support requests</documentation>
        <casePlanModel id="casePlanModel" flowable:formFieldValidation="true">
            <planItem id="planItem1" name="Register Request" definitionRef="sid-D23F3A7F-E35E-491B-A801-CAE2A8DF141D"></planItem>
            <planItem id="planItem2" name="Handle Request" definitionRef="sid-43B74981-CB5A-441E-ABDD-D0509AD0A4F8">
                <entryCriterion id="sid-C006CACE-99AC-47CD-AF5D-6102F52D856A" sentryRef="sentry1"></entryCriterion>
            </planItem>

            <sentry id="sentry1">
                <planItemOnPart id="sentryOnPart1" sourceRef="planItem1">
                    <standardEvent>complete</standardEvent>
                </planItemOnPart>
            </sentry>

            <humanTask id="sid-D23F3A7F-E35E-491B-A801-CAE2A8DF141D" name="Register Request" flowable:formFieldValidation="true"></humanTask>
            <humanTask id="sid-43B74981-CB5A-441E-ABDD-D0509AD0A4F8" name="Handle Request" flowable:formFieldValidation="true"></humanTask>
        </casePlanModel>
    </case>

    <cmmndi:CMMNDI>
        <cmmndi:CMMNDiagram id="CMMNDiagram_helpdeskCase">
            <cmmndi:CMMNShape id="CMMNShape_casePlanModel" cmmnElementRef="casePlanModel">
                <dc:Bounds height="714.0" width="718.0" x="40.0" y="40.0"></dc:Bounds>
                <cmmndi:CMMNLabel></cmmndi:CMMNLabel>
            </cmmndi:CMMNShape>
            <cmmndi:CMMNShape id="CMMNShape_planItem1" cmmnElementRef="planItem1">
                <dc:Bounds height="80.0" width="100.0" x="167.0" y="189.57881699604252"></dc:Bounds>
                <cmmndi:CMMNLabel></cmmndi:CMMNLabel>
            </cmmndi:CMMNShape>
            <cmmndi:CMMNShape id="CMMNShape_planItem2" cmmnElementRef="planItem2">
                <dc:Bounds height="80.0" width="100.0" x="555.0" y="185.0"></dc:Bounds>
                <cmmndi:CMMNLabel></cmmndi:CMMNLabel>
            </cmmndi:CMMNShape>
            <cmmndi:CMMNShape id="CMMNShape_sid-C006CACE-99AC-47CD-AF5D-6102F52D856A" cmmnElementRef="sid-C006CACE-99AC-47CD-AF5D-6102F52D856A">
                <dc:Bounds height="22.0" width="14.0" x="547.6330130435323" y="218.57881699604252"></dc:Bounds>
                <cmmndi:CMMNLabel></cmmndi:CMMNLabel>
            </cmmndi:CMMNShape>
            <cmmndi:CMMNEdge id="CMMNEdge_sid-E0987438-F457-41CD-9AD9-61A0E53EC4D0" cmmnElementRef="planItem1" targetCMMNElementRef="sid-C006CACE-99AC-47CD-AF5D-6102F52D856A">
                <di:extension>
                    <flowable:docker type="source" x="50.0" y="40.0"></flowable:docker>
                    <flowable:docker type="target" x="7.0" y="11.0"></flowable:docker>
                </di:extension>
                <di:waypoint x="266.95000000000005" y="229.57881699604252"></di:waypoint>
                <di:waypoint x="547.6330130435323" y="229.57881699604252"></di:waypoint>
                <cmmndi:CMMNLabel></cmmndi:CMMNLabel>
            </cmmndi:CMMNEdge>
        </cmmndi:CMMNDiagram>
    </cmmndi:CMMNDI>
</definitions> 

5. 控制器实现

5.1 BPMN 流程控制器

@RestController
@RequestMapping("/bpmn")
@RequiredArgsConstructor
public class BpmnController {
    private final RuntimeService runtimeService;
    private final TaskService taskService;
    private final HistoryService historyService;
    private final RepositoryService repositoryService;
    
    @PostMapping("/process/start")
    public Map<String, Object> startProcess(@RequestBody Map<String, Object> variables) {
        variables.put("initiator", variables.getOrDefault("userId", "unknown"));
        
        ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
                .processDefinitionKey("helpDeskProcess")
                .variables(variables)
                .start();
                
        Map<String, Object> result = new HashMap<>();
        result.put("processInstanceId", processInstance.getId());
        result.put("processDefinitionId", processInstance.getProcessDefinitionId());
        result.put("variables", variables);
        return result;
    }
    
    // ... 其他 BPMN 相关接口
}

5.2 CMMN 案例控制器

@RestController
@RequestMapping("/cmmn")
@RequiredArgsConstructor
public class CmmnController {
    private final CmmnRuntimeService cmmnRuntimeService;
    private final CmmnTaskService cmmnTaskService;
    private final CmmnHistoryService cmmnHistoryService;
    private final CmmnRepositoryService cmmnRepositoryService;
    
    // ... 其他代码见GitHub仓库
}

6. API 测试

6.1 BPMN 流程接口测试

# 启动流程
curl -X POST "http://localhost:8080/bpmn/process/start" \
     -H "Content-Type: application/json" \
     -d '{"userId": "john"}'

# 查询任务
curl "http://localhost:8080/bpmn/tasks?assignee=john"

# 完成任务
curl -X POST "http://localhost:8080/bpmn/tasks/{taskId}/complete" \
     -H "Content-Type: application/json" \
     -d '{"approved": true}'

6.2 CMMN 案例接口测试

# 启动案例
curl -X POST "http://localhost:8080/cmmn/helpdesk/start" \
     -H "Content-Type: application/json" \
     -d '{"userId": "john"}'

# 查询任务
curl "http://localhost:8080/cmmn/tasks?assignee=john"

# 完成任务
curl -X POST "http://localhost:8080/cmmn/tasks/69e07941-d34a-11ee-9b85-0242ac110002/complete" \
     -H "Content-Type: application/json" \
     -d '{"analysis": "网络连接问题"}'

# 查询支持组任务
curl "http://localhost:8080/cmmn/tasks?candidateGroup=support"

# 认领任务
curl -X POST "http://localhost:8080/cmmn/tasks/7a1b8c52-d34a-11ee-9b85-0242ac110002/claim?userId=mary"

7. Flowable API 说明

7.1 BPMN 相关 API

RuntimeService

  • 主要用于管理运行中的流程实例
// 启动流程实例
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
    .processDefinitionKey("helpDeskProcess")
    .variables(variables)
    .start();

// 触发信号事件
runtimeService.signalEventReceived("signalName");

// 设置流程变量
runtimeService.setVariable(processInstanceId, "varName", value);

// 删除流程实例
runtimeService.deleteProcessInstance(processInstanceId, "reason");

TaskService

  • 负责管理和操作用户任务
// 查询任务
List<Task> tasks = taskService.createTaskQuery()
    .taskAssignee("john")
    .active()
    .list();

// 完成任务
taskService.complete(taskId);

// 认领任务
taskService.claim(taskId, userId);

// 设置任务变量
taskService.setVariable(taskId, "varName", value);

RepositoryService

  • 管理流程定义和部署
// 部署流程定义
Deployment deployment = repositoryService.createDeployment()
    .addClasspathResource("processes/helpdesk.bpmn20.xml")
    .deploy();

// 查询流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
    .processDefinitionKey("helpDeskProcess")
    .latestVersion()
    .singleResult();

HistoryService

  • 访问历史数据
// 查询历史流程实例
List<HistoricProcessInstance> historicProcessInstances = historyService
    .createHistoricProcessInstanceQuery()
    .finished()
    .list();

// 查询历史任务
List<HistoricTaskInstance> historicTasks = historyService
    .createHistoricTaskInstanceQuery()
    .taskAssignee("john")
    .finished()
    .list();

7.2 CMMN 相关 API

CmmnRuntimeService

  • 管理运行中的案例实例
// 启动案例实例
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
    .caseDefinitionKey("helpDeskCase")
    .variables(variables)
    .start();

// 终止案例实例
cmmnRuntimeService.terminateCaseInstance(caseInstanceId);

// 查询案例实例
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceQuery()
    .caseInstanceId(caseInstanceId)
    .singleResult();

CmmnTaskService

  • 管理案例中的人工任务
// 查询任务
List<Task> tasks = cmmnTaskService.createTaskQuery()
    .taskAssignee("john")
    .list();

// 完成任务
cmmnTaskService.complete(taskId, variables);

// 认领任务
cmmnTaskService.claim(taskId, userId);

// 委派任务
cmmnTaskService.delegateTask(taskId, userId);

CmmnRepositoryService

  • 管理案例定义和部署
// 部署案例定义
CmmnDeployment deployment = cmmnRepositoryService.createDeployment()
    .addClasspathResource("cases/helpdesk.cmmn")
    .deploy();

// 查询案例定义
CaseDefinition caseDefinition = cmmnRepositoryService.createCaseDefinitionQuery()
    .caseDefinitionKey("helpDeskCase")
    .latestVersion()
    .singleResult();

CmmnHistoryService

  • 访问案例历史数据
// 查询历史案例实例
List<HistoricCaseInstance> historicCases = cmmnHistoryService
    .createHistoricCaseInstanceQuery()
    .finished()
    .list();

// 查询历史任务
List<HistoricTaskInstance> historicTasks = cmmnHistoryService
    .createHistoricTaskInstanceQuery()
    .taskAssignee("john")
    .finished()
    .list();

// 查询历史变量
List<HistoricVariableInstance> historicVariables = cmmnHistoryService
    .createHistoricVariableInstanceQuery()
    .caseInstanceId(caseInstanceId)
    .list();

7.3 查询 API 的通用方法

Flowable 的查询 API 提供了丰富的链式调用方法:

// 通用的查询方法
.asc()/.desc()           // 排序
.listPage(start, size)   // 分页查询
.count()                 // 计数
.singleResult()          // 获取单个结果
.list()                  // 获取列表结果

// 时间相关查询
.before(date)            // 在指定时间之前
.after(date)             // 在指定时间之后

// 排序方法
.orderByTaskCreateTime()
.orderByProcessInstanceId()
.orderByCaseInstanceId()

7.4 变量处理

Flowable 支持多种类型的变量:

// 设置变量
Map<String, Object> variables = new HashMap<>();
variables.put("stringVar", "value");
variables.put("numberVar", 123);
variables.put("dateVar", new Date());
variables.put("booleanVar", true);

// 在流程/案例中使用变量
runtimeService.setVariables(processInstanceId, variables);
cmmnRuntimeService.setVariables(caseInstanceId, variables);

// 获取变量
Map<String, Object> processVariables = runtimeService.getVariables(processInstanceId);
Map<String, Object> caseVariables = cmmnRuntimeService.getVariables(caseInstanceId);

8. BPMN 和 CMMN 的应用场景

8.1 BPMN 适用场景

  • 标准化的审批流程
  • 固定步骤的业务流程
  • 需要严格控制流程顺序的场景

8.2 CMMN 适用场景

  • 动态变化的服务请求
  • 需要灵活处理的客户案例
  • 基于知识工作者判断的任务

9. 最佳实践

  1. 选择合适的模型

    • 对于结构化、固定流程,使用 BPMN
    • 对于非结构化、动态场景,使用 CMMN
    • 可以在同一系统中结合使用两种模型
  2. 流程与案例的集成

    • BPMN 流程可以调用 CMMN 案例
    • CMMN 案例可以触发 BPMN 流程
    • 根据业务需求灵活组合
  3. 数据共享

    • 流程和案例可以共享变量
    • 使用统一的数据存储
    • 保持数据一致性

10. 注意事项

  1. 本项目使用 H2 内存数据库,重启后数据会丢失
  2. 实际生产环境建议使用 MySQL 等持久化数据库
  3. 需要根据实际业务需求调整模型
  4. 建议添加适当的权限控制机制

11. 源码地址

完整源码请访问:GitHub - flowable-springboot-starter

12. 参考文档

  • Flowable BPMN 文档
  • Flowable CMMN 文档
  • BPMN 2.0 规范
  • CMMN 1.1 规范
  • Spring Boot 官方文档

BPMN、CMMN和DMN:工作流引擎三剑客的深度对比

March 12, 2025

在现代企业数字化转型的浪潮中,工作流引擎扮演着越来越重要的角色。Flowable作为领先的开源工作流引擎,支持三种核心建模标准:BPMN(业务流程模型和标记法)、CMMN(案例管理模型和标记法)以及DMN(决策模型和标记法)。今天,让我们深入了解这三种标准的异同,以及它们各自最适合的应用场景。

Flowable三大标准概览

BPMN:结构化流程的标准语言

BPMN(Business Process Model and Notation)是最广为人知的流程建模标准。它主要用于描述具有明确开始和结束点的结构化业务流程。

BPMN的核心特点:

  • 清晰的流程控制:通过顺序流、网关等元素定义明确的执行路径
  • 丰富的事件处理:支持开始、结束、中间事件等多种事件类型
  • 活动任务管理:包含用户任务、服务任务、脚本任务等多种任务类型
  • 池和泳道:清晰展示不同参与者和组织单位的职责

BPMN的核心元素:

BPMN核心元素

  1. 流对象(Flow Objects)

    • 事件(Events):开始事件、结束事件、中间事件
    • 活动(Activities):任务、子流程、调用活动
    • 网关(Gateways):排他网关、并行网关、包容网关、事件网关
  2. 连接对象(Connecting Objects)

    • 顺序流(Sequence Flow):定义活动执行顺序
    • 消息流(Message Flow):表示跨组织通信
    • 关联(Association):连接附加信息和制品
  3. 泳道(Swimlanes)

    • 池(Pool):代表参与者或系统
    • 道(Lane):组织和分类活动
  4. 制品(Artifacts)

    • 数据对象(Data Objects):表示数据信息
    • 组(Group):文档分组用途
    • 注释(Annotation):添加文本说明

BPMN的应用场景举例:

  1. 采购审批流程

    • 固定的采购申请、部门审批、财务审核、总经理审批等步骤
    • 基于金额的条件分支(小额直接主管审批,大额需要更高层级审批)
    • 并行的询价和供应商资质审查环节
    • 定时提醒和超时升级处理
  2. 新员工入职流程

    • 顺序执行的入职手续办理
    • 并行的工位准备和账号开通任务
    • 多部门协作(HR、IT、行政等)
    • 文档管理和数据收集
  3. 产品发布流程

    • 产品设计、开发、测试、发布的固定步骤
    • 多环节的评审和审批
    • 条件判断(是否需要回退修改)
    • 里程碑节点和时间控制

CMMN:灵活应对动态场景

CMMN(Case Management Model and Notation)专注于处理非结构化、动态性强的业务场景。它采用声明式而非命令式的方法来定义流程。

CMMN的主要优势:

  • 动态任务激活:根据上下文条件动态确定任务的执行
  • 里程碑管理:通过里程碑跟踪案例进展
  • 人工决策:允许知识工作者根据具体情况做出决策
  • 计划项目:支持任务、阶段等多种计划项的灵活组合

CMMN的核心元素:

CMMN核心元素

  1. 计划项(Plan Items)

    • 任务(Tasks):人工任务、流程任务、案例任务
    • 阶段(Stages):组织和封装其他计划项
    • 里程碑(Milestones):标记重要成就点
  2. 判定准则(Sentries)

    • 进入准则(Entry Criteria):激活条件
    • 退出准则(Exit Criteria):终止条件
  3. 案例文件(Case File)

    • 案例文件项(Case File Items):业务数据
    • 案例文件项引用:数据访问和操作
  4. 计划片段(Plan Fragments)

    • 离散(Discretionary)项:可选任务
    • 规划表(Planning Tables):动态规划选项

CMMN的应用场景举例:

  1. 医疗诊疗方案

    • 根据患者症状动态调整治疗计划
    • 医生可以灵活添加检查项目
    • 基于检查结果激活不同的治疗方案
    • 随访计划的动态调整
  2. 保险理赔处理

    • 根据案件复杂度动态确定处理步骤
    • 理赔员可以根据实际情况添加调查任务
    • 多个可选的专家会诊环节
    • 基于文件收集情况触发不同处理流程
  3. 项目管理

    • 敏捷开发中的迭代规划
    • 根据项目进展动态调整任务
    • 团队成员自主选择和认领任务
    • 基于风险评估启动相应的应对措施

DMN:业务决策的智能引擎

DMN(Decision Model and Notation)专门用于建模业务决策逻辑。它可以独立使用,也可以与BPMN和CMMN无缝集成。

DMN的关键特性:

  • 决策表:直观展示输入条件和输出结果的映射关系
  • 决策需求图:展示决策之间的依赖关系
  • 表达式语言:支持复杂的业务规则表达
  • 可重用性:决策逻辑可在不同流程中重复使用

DMN的核心元素:

DMN核心元素

  1. 决策需求图(DRD)组件

    • 决策(Decision):核心决策节点
    • 输入数据(Input Data):外部数据源
    • 知识源(Knowledge Source):决策依据
    • 业务知识模型(Business Knowledge Model):可重用决策逻辑
  2. 决策表(Decision Table)元素

    • 输入(Input):条件列
    • 输出(Output):结果列
    • 规则(Rules):条件和结果的映射行
    • 命中策略(Hit Policy):规则匹配策略
  3. 表达式语言

    • FEEL表达式:标准决策表达式语言
    • 简单表达式:基础运算和比较
    • 复杂表达式:函数和条件逻辑
  4. 数据类型

    • 基本类型:数字、字符串、布尔等
    • 复合类型:列表、上下文等
    • 自定义类型:业务特定数据类型

DMN的应用场景举例:

  1. 信贷审批决策

    • 基于收入、信用记录等多个指标的评分
    • 不同产品的利率定价规则
    • 贷款额度的自动计算
    • 风险等级的综合评定
  2. 保险产品定价

    • 多维度的保费计算规则
    • 各种优惠政策的叠加计算
    • 风险因素的权重评估
    • 自动化的核保决策
  3. 营销活动规则

    • 客户分群策略
    • 促销方案的匹配规则
    • 优惠券发放条件
    • 会员等级升降规则

详细对比

下表详细对比了BPMN、CMMN和DMN在不同维度上的特点:

对比维度BPMNCMMNDMN
核心目标结构化业务流程建模非结构化案例管理业务决策规则建模
建模方法命令式(规定具体执行步骤)声明式(定义目标和约束)声明式(定义决策规则)
执行特点固定的执行路径动态的执行路径基于规则的推导
灵活性低(流程一旦定义较难变更)高(可动态调整任务)中(规则可动态更新)
可预测性高(流程路径明确)低(执行路径不确定)高(规则结果确定)
使用场景标准化业务流程知识密集型工作复杂业务决策
主要元素事件、活动、网关计划项、判定准则、里程碑决策表、决策需求图
建模难度中等较高较低
维护成本较高(需要修改流程图)中等(规则可动态调整)较低(易于更新规则)
业务适用性固定流程业务动态响应业务规则驱动业务

三者的协同应用

在实际应用中,这三种标准常常协同工作:

  • BPMN处理主流程框架
  • CMMN处理需要灵活应对的子流程或特殊场景
  • DMN处理流程中的决策点

选择建议

  1. 当流程具有明确的步骤和路径时,选择BPMN
  2. 当业务场景需要更多灵活性和动态响应时,使用CMMN
  3. 当需要标准化决策逻辑时,采用DMN
  4. 在复杂项目中,可以组合使用这三种标准,发挥各自优势

用对工具,事半功倍

BPMN、CMMN和DMN各自针对不同的业务场景,共同构成了完整的工作流解决方案。理解它们的差异和适用场景,对于选择合适的工具实现业务目标至关重要。在Flowable中,这三种标准的无缝集成为企业提供了强大而灵活的工作流管理能力。

Next →
Flowable 中文文档
文档
指南Java文档
法律
免责声明政策开源协议
联系方式
邮箱: [email protected]
版权 © 2025 Flowable AG. 中文文档基于 Apache License 2.0 协议翻译