流程建模这部分内容,我会从流程定义、事件节点定义、任务节点定义和网关节点定义四大部分来展开介绍。让你了解如何设计一个流程建模语言,包括工作流的定义、节点的定义和分类(事件、任务和网关)、运行时数据的定义以及事件类型定义;

流程定义这一节内容,主要介绍工作流、节点和运行时数据的JSON结构怎么设计。

# 流程定义

传统BPMN采用XML的语言来定义流程,虽然描述很完整,但是其描述复杂,比较难上手理解。

区别于传统做法,我们采用Json这种互联网上更通用的格式来定义流程,这种数据格式在系统之间也更容易传递交互、更容易理解。

如下是,一个完整的工作流定义。

需要注意,workflow流程定义是一个静态数据,其运行时的数据execution,即流程实例则是动态数据。一个流程(workflow)被执行时,会产生流程实例(execution),这个流程实例也会包含流程定义的结构,同时也包含此次运行时的数据,他们是一对多的关系。

以下数据工作流定义的字段结构:

# workflow定义

属性 说明 是否必须
id 工作流ID,唯一标识工作流的身份
name 工作流名称
description 工作流描述
version 工作流版本
startTaskInstId 工作流开始运行的节点。默认为空,表示从流程实例的节点开始运行。
endTaskInstId 工作流运行结束时的节点。默认为空,表示运行到流程实例结束为止。
engine 引擎类型,支持:快速模式(fast)、标准模式(standard),默认是标准模式。两者区别见《引擎执行模式》一节内容。
status 工作流状态,状态轮转见后面内容。
creator 工作流创建人
executor 工作流执行触发人
timeout 工作流执行的超时时间,默认为0表示没有时间限制。
inputDataSchema 声明和验证工作流的输入数据。
outputDataSchema 声明和验证工作流的输出数据
error 工作流执行的错误信息
extension 扩展字段可以用来设置并发限制等附加信息,以此来增强工作流的定义
metadata 元数据主要用来存储工作流核心定义之外的信息,例如作者邮件、环境信息、分类等
nodes node节点数组

其Json结构参考如下:

{
    "id": "1541815603606036480",
    "name": "Unknown",
    "description": "",
    "startTaskInstId": "",
    "endTaskInstId": "",
    "engine": "fast",
    "status": "",
    "creator": "admin",
    "executor": "",
    "timeout": 0,
    "inputDataSchema": {
        "string_value": {
            "type": "string",
            "max": 100,
            "min": 10,
            "default": "",
            "required": true
        }
    },
    "outputDataSchema": {
        "string_value": {
            "type": "string",
            "max": 100,
            "min": 10,
            "default": "",
            "required": true
        }
    },
    "error": "",
    "extension": {
        "rateLimit": 10
    },
    "metadata": {
        "email": "xxx@qq.com"
    },
    "nodes": [
        {
            "instId": "xxx",
            "name": "脚本任务执行",
            "type": "task",
            "description": "执行脚本任务",
            "template": "script",
            "positions": [],
            "connections": [],
            "parameters": {
                "code": {
                    "label": "代码",
                    "type": "string",
                    "value": "",
                    "default": "",
                    "required": true
                }
            },
            "errorHandler": {},
            "loopHandler": {},
            "timeout": 0,
            "isIgnore": false,
            "runtimes": [
                {
                    "index": 0,
                    "startAt": 1700063265000,
                    "endAt": 1700063307000,
                    "status": "done",
                    "error": "",
                    "input": {
                        "code": "xxx"
                    },
                    "output": "ok"
                }
            ]
        }
    ]
}

各个属性介绍如下:

id

工作流的ID,可以使用数据库的自增长生成的唯一ID,也可以使用类似UUID算法或者twitter的雪花算法生成唯一的ID。

name

工作流名称。

description

可用于提供有关工作流的更多信息。

version

可以指定工作流的具体版本,版本格式采用通用的格式:主版本号.次版本号.修订号,版本号递增规则如下:。

  • 主版本号:当出现不兼容的大版本变动

  • 次版本号:当做了向下兼容的功能性新增

  • 修订号:当做了向下兼容的问题修正

startTaskInstId

指定工作流执行时起始运行节点,默认情况下为空表示由默认的起始节点开始。

endTaskInstId

指定工作流执行的终止节点,即执行到该节点,则停止执行。

engine

工作流运行时选择使用的引擎类型,分别有快速模式和标准模式。两种模式执行有区别,快速模式适合执行时间短、运行速度快、任务量大的业务流程,例如API服务等。标准模式适合长时间运行的业务流程,例如审批等。两者的具体在下一章会详细介绍。

status

表示工作流的运行状态,详细见下一章流程实例生命周期介绍:

image-20240304094655399

creator

表示工作流的创建人。

executor

表示触发工作流执行的人,例如在表单提交时可以通过登录态获取提交表单的用户,默认则是工作流创建人。

timeout

指定工作流运行的最长时间,超过该时间后,引擎会强制终止流程中所有运行中的任务实例,并将status字段设置为已停止状态。

inputDataSchema

用于声明和验证工作流接收的参数类型,如果不符合声明的要求,则无法触发工作流运行。其格式参考如下。

主要定义输入参数的类型,包括基本的字符串(string)、整数(integer)、浮点数(float)、布尔值(boolean),以及默认值、是否必填等校验信息。这样可以在触发工作流运行之前,检查输入参数是否符合要求。这个在设计API类型的工作流时用的最多。例如下面的例子:

{
    "input_string_value": {
        "type": "string",
        "max": 100,
        "min": 10,
        "default": "",
        "required": true
    },
    "input_integer_value": {
        "type": "integer",
        "max": 200,
        "min": 0,
        "default": 0,
        "required": false
    },
    "input_float_value": {
        "type": "float",
        "max": 1.0,
        "min": 0.0,
        "default": 1.0,
        "required": false
    },
    "input_boolean_value": {
        "type": "boolean",
        "default": false,
        "required": false
    }
}

outputDataSchema

功能跟inputDataSchema类型,主要用于声明校验工作流的输出结果。如果不符合声明的要求,则会导致报错信息(存储在error字段),并修改status状态为错误。

error

记录工作流在运行时发生异常的错误信息,可用于排查故障。

extension

该属性用于增强扩充工作流功能的定义,例如设置工作流的并发度,或者设置工作流的运行KPI指标等信息。

例如下面定义了每秒钟该工作流同时能触发运行的工作流实例个数,类似于API限频控制每秒并发数。

{"rateLimit":10}

metadata

该属性存储一些工作流的元数据,这些元数据不影响工作流的运行,例如工作流创建者的联系方式等信息。

例如:

{"email":"xxx@qq.com"}

# node节点定义

如下图所示,是BPMN建模语言中包含的各种基础元素,包括任务、事件以及网关元素。BPMN建模语言虽然描述的元素非常丰富全面,但我们在实际的使用过程中,发现绝大部分元素是几乎用不到的。本着二八原则和实用角度出发,我们对BPMN的任务、事件和网关元素进行了精简,剥离出使用最频繁的元素进行定义总结。

下面构成一个流程的四个基础元素:

元素 符号 描述
任务(Task) BPMN symbol - Task 流程中执行具体任务的节点
事件(Event) BPMN symbol - Start event 在流程实例运行过程中发生的事件,例如开始、结束等
网关(Gateway) BPMN symbol - symbol 控制流程的数据流转和路由决策方向
箭头(Flow) BPMN symbol - flow 箭头用于说明节点之间的流向以及依赖关系,这里直接用connections定义

下面是节点的各字段定义:

备注:在这里,我们把前面任务、事件和网关三个基础元素都统一用node节点来定义JSON结构,其区别通过type字段来标识,不同类型的节点,其区别在parameters里体现。

属性 说明 是否必须
instId 流程中每个节点都有唯一的id来标识
name 节点名称
type 节点类型,包括:任务(Task)、事件(Event)和网关(Gateway)
description 节点描述
template 节点模板,不同模板类型的节点代表执行不同的功能
positions 节点在流程设计器平面上的x、y坐标位置
connections 当前节点指向的下一个节点(大于等于0),最后的叶子节点为空数组
parameters 不同节点模板,其输入参数不一样,是一个Json的结构的对象。
errorHandler 错误处理器
loopHandler 循环处理器
timeout 节点运行的最大时长
isIgnore 当前节点是否忽略运行。如果设置为true,则运行时,引擎会检测到该标识并跳过该节点不执行,接着执行下一个节点
runtimes 运行时数据,它是一个runtime结构体数组。每个runtime包括运行时间、运行时的入参数据、输出结果等

其Json结构参考如下:

{
    "instId": "xxx",
    "name": "脚本任务执行",
    "type": "Task",
    "description": "执行脚本任务",
    "template": "script",
    "positions": [],
    "connections": [],
    "parameters": {
        "code": {
            "label": "代码",
            "type": "string",
            "value": "",
            "default": "",
            "required": true
        }
    },
    "errorHandler": {},
    "loopHandler": {},
    "timeout": 0,
    "isIgnore": false,
    "runtimes": []
}

同样的,这里节点(node)定义是静态数据,其运行时会产生节点实例这种动态数据(nodeInst),每个节点实例都会包含节点定义的数据结构,也包含运行时的数据。两者也是一对多的关系。

instId

任务的实例ID,流程中每个节点的实例ID都是唯一的,可以采用UUID算法生成或者使用twitter的雪花算法生成,这里推荐后者。因为雪花算法生成的ID唯一且成增长趋势,可以避免ID随机导致的数据库主键插入顺序频繁排序引致的低效率问题,而且也方便后续分表时作为全局唯一的分布式主键ID。

name

描述节点名称

type

描述节点类型,包括:任务(Task)、事件(Event)和网关(Gateway)

description

描述节点的功能

template

节点的类型,包括:起始节点、节点、数据流控制节点三大类,更具体可以参考下一章内容。

positions

描述节点在流程设计器上坐标位置,主要用于在浏览器上图形化渲染展示。

connections

当前节点指向的下一个节点实例ID,如果数组有多个实例ID,则可能是并行节点。如果是流程的叶子节点,即最后一个节点,则该属性为空数组。

parameters

定义当前节点的入参属性,采用字典结构,每个入参key属性对应label、type、value、default、required五个字段,例如下面的json结构。

  • label

    描述入参的名称

  • type

    描述入参的类型,包括:字符串(string)、整数(integer)、浮点数(float)、布尔值(boolean)等

  • key

    描述入参的唯一key值标识。注意节点中的parameters数组里的成员每个key值都是不重复的,这个主要用于定位表示字段的位置。在后面介绍的变量表达式中也会用到,用于引擎在运行时定位节点的参数位置。

  • value

    描述入参的值。

  • default

    描述入参的默认值。

  • required

    描述入参是否必填

{
    "name": {
        "label": "名字",
        "type": "string",
        "value": "zhangsan",
        "default": "无",
        "required": true
    },
    "age": {
        "label": "年龄",
        "type": "integer",
        "value": 30,
        "default": 10,
        "required": false
    }
}

errorHandler 错误节点

其JSON结构如下:

{
    "operation": "stop",
    "retryCount": 0,
    "retryInterval": 0,
    "exception_branch":"xxx"
}

其中operation字段支持的动作包括:

  • stop:发生错误,则直接停止往下执行
  • ignore:发生错误,忽略,继续往下执行
  • catch:发生错误,捕获并响应,执行异常分支的流程(exception_branch分支指向的节点)
  • retry:发生错误,重试retryCount次,每次之间间隔是retryInterval秒

loopHandler循环处理器

其JSON结构如下:

{
    "loopType": "for",
    "maxIteration": 1000,
    "stopCond": {
        "type": "String",
        "value1": "",
        "operation": "equal",
        "value2": ""
    }
}

其中loopType支持for循环和do...while循环

两种循环模式都支持设置最大循环次数maxIteration。

其中do...while循环需要设置循环终止的条件,即stopCond字段。

终止条件的判断支持常见的字符串、数字对比方式。

timeout

设置当前节点运行的最长时间,超过该时间,引擎会强制终止,并设置当前任务实例为超时状态。

isIgnore

设置当前节点在运行时是否要忽略不执行,直接运行下一个节点。这个功能一般用在调试的时候用。

runtimes

节点的运行时数据,如果节点被多次运行,则会生成多个runtime结构体数据,详细见下面介绍。

# runtime定义

属性 说明
index 索引位置。由于流程中一个节点可能被执行多次(例如审批打回或循环),这里通过index递增来区分不同task实例,它是依次递增的。
startAt 任务实例运行开始时间
endAt 任务实例运行结束时间
status 任务实例的状态
error 任务实例运行时发生异常时的错误信息
input 运行时的入参数据,跟task结构体的parameters一一对应,是key、value的字典结构。前面node节点定义中的parameters是定义时的静态数据,可能是常量、也可能是变量表达式(运行时解析)。而这里的input则是节点运行时的动态数据,可能是常量、也可能是变量表达式运行时解析后的数据。两种是一个静态、一个动态数据的区别。
output 任务实例运行时的输出结果。其数据类型可以是字符串、数字、布尔值、数组、或复杂的对象结构。

其Json结构参考如下:

{
    "index": 0,
    "startAt": 1700063265000,
    "endAt": 1700063307000,
    "status": "done",
    "error": "",
    "input": {
        "code": "xxx"
    },
    "output": "ok"
}

index

描述任务实例运行的顺序,如果节点只运行一次,则index为0,即第一个被运行的任务实例。如果节点被多次运行,例如在审批打回或者循环的场景,则index依次递增。也就是说,runtime这里存储的是任务的运行时数据,即任务实例,这里可以是多个。

startAt

任务实例开始运行的时间,格式采用UNIX毫秒时间戳。

endAt

任务实例运行结束的时间,格式采用UNIX毫秒时间戳。跟startAt相减可以精确计算出任务实例毫秒级的运行耗时。

status

任务实例的状态,详细见下一章任务实例生命周期的介绍。

image-20240304093637277

input

任务实例运行时的入参数据,这里跟task结构体的parameters一一对应。

例如task的parameters定义如下:

{
    "name": {
        "label": "名字",
        "type": "string",
        "value": "zhangsan",
        "default": "无",
        "required": true
    },
    "age": {
        "label": "年龄",
        "type": "integer",
        "value": 30,
        "default": 10,
        "required": false
    }
}

那么任务实例在运行时的input结构如下,采用key、value的字典结构。

{"name":"zhangsan", "age":30}

之所以还要记录任务实例运行时的入参数据,主要是因为在定义task任务的parameters时,它的参数value可以是变量表达式的形式,即在运行时根据上下文数据动态解析,并不是都是固定常量。

output

任务实例运行的输出,其数据类型可以是字符串、数字、布尔值、数组、或复杂的对象结构。

最后更新: 7/21/2024, 9:20:19 AM