工作流 Flowable 全流程
第一章,Flowable 关键定义
我们将关键概念分为三个阶段:设计态(Design Time)、启动态(Deploy Time)和运行态(Run Time)。
理解这些概念的核心在于掌握它们之间的**“从静态文件到动态数据”**的转化过程。
一、 设计态:绘制蓝图
在这个阶段,一切都只是存储在数据库或文件系统中的“草稿”或“图纸”,静止不动。
1. BPMN 2.0 (Business Process Model and Notation)
- 定义:这是一套国际通用的业务流程建模符号标准。
- 实质:虽然你在前端看到的是画出来的图(圆圈、方框、箭头),但在底层,它是一段标准的 XML 代码。Flowable 引擎就是通过解析这段 XML 来理解你要怎么走流程的。
- 地位:它是人和机器沟通的共同语言。
2. 流程模型 (Model)
- 对应表:
ACT_RE_MODEL - 定义:这是流程图的设计草稿。
- 关键点:它还是“半成品”。你可以在设计器里反复修改它,保存它(对应
REV_版本号增加)。此时它还不能被运行,只是数据库里的一串 JSON 或 XML 字符串。
3. 表单 (Form)
- 定义:流程流转过程中,用户与系统交互的界面载体。
- 关系:
- 挂载:表单通常挂载在“开始节点”或“用户任务节点”上。
- 数据:表单里的输入项(Input),最终会转化为流程的变量 (Variable) 存储在引擎中。
二、 启动态:发布生效 (Deployment)
这个阶段是将“草稿”变成“正式规章制度”的过程。
4. 流程部署 (Deployment)
- 对应表:
ACT_RE_DEPLOYMENT - 定义:这是一个动作,也是一个容器。当你点击“发布”时,引擎会将你的模型打包成一个“部署包”。
- 作用:它像一个快照(Snapshot),把这一刻的 XML、图片等资源永久固化下来。
5. 流程定义 (Process Definition)
- 对应表:
ACT_RE_PROCDEF - 定义:这是被引擎解析后的规则对象。
- 类比:在 Java 中,它相当于
Class(类)。 - 关键点:它是只读的。一旦部署产生,就不能修改。如果你改了图重新部署,会生成一个新的流程定义版本(Version 2),旧版本(Version 1)依然存在但不再是最新。
- 作用:后续所有的流程实例,都是根据这个“模具”克隆出来的。
三、 运行态:流转执行 (Runtime)
这个阶段是真正的业务流转,数据开始在表中产生和消亡。
6. 流程实例 (Process Instance)
- 对应表:
ACT_RU_EXECUTION(根记录) - 定义:这是流程定义的一次具体执行。
- 类比:在 Java 中,它相当于
new Object()(对象)。 - 场景:张三发起的请假单是一个实例,李四发起的请假单是另一个实例。它们共用同一个“流程定义”,但互不干扰。
7. 执行流 (Execution / Child Execution)
-
对应表:
ACT_RU_EXECUTION -
定义:这是引擎内部的“指针”或“令牌”。它标记了流程当前走到哪一步了。
-
关键关系:
- 单线流程:如果是 A->B->C 这种简单流程,流程实例 ID = 执行流 ID。
- 并发流程:如果遇到并行网关(同时走 A 和 B),流程实例会分裂出两个子执行流 (Child Execution)。
- 父执行流(Root):代表整个实例,原地不动。
- 子执行流 A:指向任务 A。
- 子执行流 B:指向任务 B。
-
理解:Execution 是用来解决“分身术”问题的。
8. 任务 (Task / UserTask)
- 对应表:
ACT_RU_TASK - 定义:流程运行中需要人工介入的环节。
- 特征:它会出现在用户的“待办列表”中。只有当用户调用
complete()方法后,该任务才会消失,执行流(Execution)才会流向下一个节点。
9. 变量 (Variable)
- 对应表:
ACT_RU_VARIABLE - 定义:流程实例携带的数据包。
- 类型:
- 全局变量:跟着流程实例走,任何节点都能读写(如:请假天数)。
- 局部变量:只绑定在某个任务或子执行流上,任务结束就消失(如:临时的审批备注)。
四、 流程节点与事件
构成 BPMN 图的具体元素。
10. 流程节点 (Flow Element)
- 定义:构成流程图的基本单元。包括任务(Task)、网关(Gateway)、事件(Event)等。
11. 信号事件 (Signal Event)
-
定义:广播模式 (Broadcast)。
-
机制:类似于“发令枪”或“收音机”。
-
一个流程抛出一个信号(Signal Throw)。
-
所有正在等待该信号的流程实例(Signal Catch)都会被触发。
-
场景:管理员发布一条“政策变更”信号,所有正在进行的审批流程自动跳转到“合规检查”节点。是一对多的。
12. 消息事件 (Message Event)
-
定义:点对点模式 (P2P)。
-
机制:类似于“发邮件”。
-
消息必须有明确的接收者(通常通过 Message Name 匹配)。
-
一个消息只能触发一个特定的流程实例或启动一个新实例。
-
场景:支付系统回调,通知订单流程“支付成功”。是一对一的。
13. 网关 (Gateway)
- 定义:流程的交通指挥官。
- 常见类型:
- 排他网关 (Exclusive):X 分支。
if-else,多条路只走一条(如:请假 < 3 天走经理,>= 3 天走总监)。 - 并行网关 (Parallel):
Fork-Join,所有路同时走,必须都到了才能汇合(如:会签,需要财务和行政同时审批)。
- 排他网关 (Exclusive):X 分支。
关键概念关系图谱
为了方便记忆,请记住这个层级关系:
- Model (草稿) --(发布)–> Deployment (部署包)
- Deployment --(包含)–> Process Definition (Java类/规则)
- Process Definition --(实例化)–> Process Instance (Java对象/具体单据)
- Process Instance --(包含)–> Execution (当前指针)
- Execution --(指向)–> Task (当前要做的事)
- Task + Process Instance --(携带)–> Variables (业务数据)
第二章,Flowable 关键表结构
Flowable 的数据库表结构设计非常精妙,遵循了一套严格的命名规范。理解这些表及其字段的含义,是你排查问题、进行二次开发(如自定义流程监控、报表统计)的基础。
表名通常以 ACT_ 开头(源自 Activiti),第二部分由两个字母组成,代表表的生命周期或用途:
ACT_RE_*(Repository):存储库。存放静态资源,如流程定义、流程图、部署信息。ACT_RU_*(Runtime):运行时。存放流程运行期间产生的数据(正在跑的流程、待办任务)。流程结束,数据即删。ACT_HI_*(History):历史。存放所有数据(已完成的 + 正在运行的)。用于审计和查询历史。ACT_GE_*(General):通用。存放通用数据,如二进制文件(XML/图片)。
一、 静态资源层 (Repository - ACT_RE_*)
这一层是流程的“模具”和“底座”。
1. ACT_RE_DEPLOYMENT (部署信息表)
每一次调用 deploy() 都会在这里产生一条记录。它像一个“包裹”。
ID_: [主键] 部署 ID。后续所有资源文件都通过这个 ID 找到归属。NAME_: 部署名称(如 “请假流程-2026版”)。DEPLOY_TIME_: 部署时间。
2. ACT_RE_PROCDEF (流程定义表 - 核心)
引擎解析 XML 后生成的规则类。
ID_: [主键] 格式通常为key:version:randomUUID(如leave:1:25001)。这是发起流程时startProcessInstanceById用的 ID。KEY_: [关键] 流程标识(如leave)。这是 XML 中<process id="leave">定义的。业务开发通常用这个 ID 来startProcessInstanceByKey。VERSION_: 版本号。Flowable 自动维护,每次部署相同的 Key,版本号 +1。DEPLOYMENT_ID_: [外键] 关联ACT_RE_DEPLOYMENT.ID_。表示这个定义是哪次部署带进来的。RESOURCE_NAME_: 关联的 XML 文件名(如leave.bpmn20.xml)。DGRM_RESOURCE_NAME_: 关联的图片文件名(如leave.png)。SUSPENSION_STATE_: 挂起状态(1=激活,2=挂起)。挂起后无法发起新流程。
3. ACT_RE_MODEL (流程模型表 - 设计草稿)
这张表存储流程设计器(Modeler)中的设计草稿。它是可编辑、可变的,是流程定义的前身。
-
ID_: [主键] 模型 ID。模型的唯一标识。
-
KEY_: [关键] 模型标识(如
leave)。对应流程设计图中的 process id。 -
NAME_: 模型名称。在设计器列表中显示的名称(如 “请假流程草稿”)。
-
VERSION_: 版本号。这是设计草稿的版本(不同于流程定义的版本)。通常每次在设计器中点击“保存”,该版本号会增加。
-
META_INFO_: 元数据。存储 JSON 格式的扩展信息。由于 Flowable 原生字段较少,业务系统通常将流程描述、关联的表单 ID、图标等自定义属性序列化后存储在此字段中。
-
DEPLOYMENT_ID_: [外键] 关联
ACT_RE_DEPLOYMENT.ID_。 -
若为空 (
NULL):表示该模型仅为草稿,尚未发布,引擎无法运行它。 -
若有值:表示该模型已执行过部署操作,指向生成的那个部署包 ID。
-
EDITOR_SOURCE_VALUE_ID_: [外键] 关联
ACT_GE_BYTEARRAY.ID_。指向存储模型源文件(通常为设计器专用的 JSON 数据,而非标准 XML)的二进制记录。前端设计器回显流程图时,读取的是这个字段指向的数据。
二、 运行时层 (Runtime - ACT_RU_*)
这一层是流程的“现场”。请记住:当流程结束,这里的记录会被物理删除!
4. ACT_RU_EXECUTION (执行实例表 - 引擎的心脏)
它是流程运行的指针和树干。
-
ID_: [主键] 执行流 ID。 -
PROC_INST_ID_: [关键] 流程实例 ID(Root ID)。在一个简单的单线流程中,根执行流的ID_等于PROC_INST_ID_。 -
PARENT_ID_: 父执行流 ID。用于构建树状结构。 -
场景:遇到并行网关时,会分裂出子执行流,子执行流的
PARENT_ID_指向根执行流。 -
PROC_DEF_ID_: [外键] 指向ACT_RE_PROCDEF.ID_。表明当前是在跑哪个版本的流程。 -
ACT_ID_: [关键] 当前停留的节点 ID(XML 中的UserTask_01)。引擎重启后,靠这个字段知道从哪里继续跑。 -
IS_ACTIVE_: 是否活跃。 -
BUSINESS_KEY_: 业务主键(如请假单 ID)。通常只存放在根执行流中。
5. ACT_RU_TASK (运行时任务表 - 待办核心)
记录所有未完成的人工任务。
ID_: [主键] 任务 ID。EXECUTION_ID_: [外键] 关联ACT_RU_EXECUTION.ID_。表示这个任务属于哪个执行分支。PROC_INST_ID_: 流程实例 ID(为了查询方便冗余存储)。NAME_: 任务名称(如 “经理审批”)。TASK_DEF_KEY_: [关键] 节点定义的 Key(如task_manager_approve)。开发中常用来判断当前任务处于哪个环节。ASSIGNEE_: 办理人 ID。如果为空,说明是“候选任务”,需要签收。CREATE_TIME_: 任务创建时间。SUSPENSION_STATE_: 1=激活,2=挂起。
6. ACT_RU_VARIABLE (运行时变量表)
存储流程运行期间的 Key-Value 数据。
NAME_: 变量名(如days)。TYPE_: 变量类型(string, integer, boolean, serializable)。TEXT_: 存储字符串值。LONG_: 存储数字值。PROC_INST_ID_: 所属流程实例。TASK_ID_: [关键] 如果有值,说明是局部变量 (Local),只在当前任务有效;如果为空,说明是全局变量。
三、 历史层 (History - ACT_HI_*)
这一层是流程的“档案室”。数据永远保留(除非手动清除)。
7. ACT_HI_PROCINST (历史流程实例表)
记录一次请假申请的完整生命周期。
ID_: 等同于运行时的PROC_INST_ID_。START_USER_ID_: 发起人 ID。START_TIME_: 开始时间。END_TIME_: 结束时间。如果为空,说明流程还在运行中。DURATION_: 耗时(毫秒)。DELETE_REASON_: 结束原因。正常结束为 null;如果是被“驳回/取消”,这里会记录原因。
8. ACT_HI_TASKINST (历史任务实例表)
记录每一个任务的执行情况(包括已办和待办)。
ID_: 任务 ID。ASSIGNEE_: 实际办理人。START_TIME_: 任务创建时间(进入节点的时间)。END_TIME_: 任务完成时间(点击“通过”的时间)。DURATION_: 审批耗时(END - START)。这是做绩效统计(谁审批最慢)的核心字段。
9. ACT_HI_ACTINST (历史活动实例表)
最详细的轨迹表。它记录了流程走过的每一个“脚印”,包括 StartEvent, EndEvent, SequenceFlow (连线), Gateway (网关), UserTask 等。
ACT_ID_: 节点 ID。ACT_TYPE_: 节点类型(userTask,exclusiveGateway…)。- 作用:前端绘制“流程进度高亮图”时,就是查这张表,看哪些节点有
END_TIME_,就把哪些节点标红。
四、 通用数据层 (General - ACT_GE_*)
10. ACT_GE_BYTEARRAY (二进制资源表)
这是数据库里的“文件系统”。
ID_: 资源 ID。NAME_: 资源名称(source,leave.bpmn,leave.png)。BYTES_: [BLOB] 文件内容的二进制流。DEPLOYMENT_ID_: [关联]- 如果有值:说明是运行态资源(部署产生的
leave.bpmn),引擎用它。 - 如果为 NULL:说明是设计态资源(草稿
source),设计器用它。
五、 表之间字段的联系
理解这些联系是写复杂 SQL 查询的关键。
- 定义与部署的关联:
ACT_RE_PROCDEF.DEPLOYMENT_ID_->ACT_RE_DEPLOYMENT.ID_
(定义属于哪次部署) - 资源与部署的关联:
ACT_GE_BYTEARRAY.DEPLOYMENT_ID_->ACT_RE_DEPLOYMENT.ID_
(XML文件属于哪次部署) - 运行实例与定义的关联:
ACT_RU_EXECUTION.PROC_DEF_ID_->ACT_RE_PROCDEF.ID_
(这个实例是根据哪个规则跑的) - 任务与实例的关联:
ACT_RU_TASK.PROC_INST_ID_->ACT_RU_EXECUTION.PROC_INST_ID_
(任务属于哪个流程单子)ACT_RU_TASK.EXECUTION_ID_->ACT_RU_EXECUTION.ID_
(任务属于树上的哪个分支) - 历史与运行时的关联:
ACT_HI_PROCINST.ID_==ACT_RU_EXECUTION.PROC_INST_ID_
(ID 是通用的,只是数据搬家了)
第三章,Flowable 核心类与用法
RepositoryService
RepositoryService 是 Flowable 引擎中专门用于管理静态资源的服务接口。它的核心职责是对流程定义文件(.bpmn20.xml)、图片资源、部署包(Deployment)以及模型(Model)进行CRUD(增删改查)操作。
它主要操作以下几张核心数据库表:
- ACT_RE_DEPLOYMENT:部署信息表(一次部署包含多个资源)。
- ACT_RE_PROCDEF:流程定义表(解析后的规则数据)。
- ACT_GE_BYTEARRAY:二进制资源表(存储 XML 文件流、图片流)。
- ACT_RE_MODEL:流程设计模型表(Web 设计器的草稿)。
以下是 RepositoryService 最常见的用法实例,分为部署流程、查询定义、读取资源、流程控制和模型管理五个维度。
1. 部署流程 (Deployment)
这是最基础的操作,将本地的 .bpmn 文件发布到引擎数据库中。
1.1 Classpath 资源部署
最常见的方式,读取 resources 目录下的文件。
Deployment deployment = repositoryService.createDeployment()
.name("请假流程部署") // 设置部署名称
.key("leave_deployment") // 设置部署Key(可选)
.addClasspathResource("processes/leave.bpmn20.xml") // 读取文件
.addClasspathResource("processes/leave.png") // 读取图片(可选)
.deploy(); // 执行入库操作
System.out.println("部署ID: " + deployment.getId());
- 数据库影响:向
ACT_RE_DEPLOYMENT插入 1 条记录,向ACT_GE_BYTEARRAY插入 2 条记录(XML+图片),向ACT_RE_PROCDEF插入 1 条记录。
1.2 字符串/流式部署
适用于动态生成 XML 或从前端接收 XML 字符串的场景。
String bpmnXml = "<?xml version='1.0' ..."; // 前端传来的 XML 字符串
Deployment deployment = repositoryService.createDeployment()
.name("动态部署")
.addString("dynamic_process.bpmn", bpmnXml) // 直接存字符串
.deploy();
2. 查询流程定义 (Process Definition)
查询 ACT_RE_PROCDEF 表,获取已部署的流程规则信息。
2.1 基础查询
// 查询所有“leave”流程的最新版本
List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("leave") // 流程图中的 id="leave"
.latestVersion() // 只查最新版本(排除旧版本)
.orderByProcessDefinitionVersion().desc()
.list();
for (ProcessDefinition pd : list) {
System.out.println("ID: " + pd.getId()); // 格式:leave:1:xxxx
System.out.println("Version: " + pd.getVersion());
System.out.println("Name: " + pd.getName());
}
2.2 挂起与激活查询
// 查询所有处于“挂起”状态(禁止发起)的流程定义
List<ProcessDefinition> suspendedList = repositoryService.createProcessDefinitionQuery()
.suspended()
.list();
3. 读取静态资源 (Resources)
用于查看流程图文件或图片流,常用于前端流程图回显。
3.1 获取流程图 XML 流
String processDefinitionId = "leave:1:10001"; // 从 ProcessDefinition 获取
// 获取 BPMN XML 输入流
InputStream bpmnStream = repositoryService.getProcessModel(processDefinitionId);
// 转换为字符串打印或传给前端
// IoUtil.readUtf8(bpmnStream);
3.2 获取流程图图片流
// 获取流程定义对象
ProcessDefinition pd = repositoryService.createProcessDefinitionQuery()
.processDefinitionId("leave:1:10001")
.singleResult();
String diagramResourceName = pd.getDiagramResourceName(); // 获取图片文件名
String deploymentId = pd.getDeploymentId();
// 根据部署ID和资源名称读取流
InputStream imageStream = repositoryService.getResourceAsStream(deploymentId, diagramResourceName);
3.3 获取结构化模型 (BpmnModel)
这是非常重要的一个方法,用于在代码中解析流程图的节点信息(例如:获取下一个节点是不是用户任务,有没有扩展属性 signEnable)。
// 解析为 Java 对象结构
BpmnModel bpmnModel = repositoryService.getBpmnModel("leave:1:10001");
// 访问具体节点
FlowElement flowElement = bpmnModel.getFlowElement("UserTask_01");
if (flowElement instanceof UserTask) {
UserTask task = (UserTask) flowElement;
System.out.println("办理人表达式: " + task.getAssignee());
// 读取扩展属性
List<ExtensionElement> signs = task.getExtensionElements().get("signEnable");
}
4. 流程定义控制 (Control)
控制流程是否允许被发起。
4.1 挂起流程定义
挂起后,用户无法发起该流程的新实例,但正在运行的实例不受影响(除非设置级联挂起)。
// 挂起流程定义(ID),参数2:是否挂起该定义下的所有流程实例,参数3:生效时间(null代表立即)
repositoryService.suspendProcessDefinitionById("leave:1:10001", true, null);
4.2 激活流程定义
repositoryService.activateProcessDefinitionById("leave:1:10001", true, null);
4.3 删除部署
删除部署会级联删除定义、资源文件。
String deploymentId = "10001";
// 参数2 cascade: true 表示级联删除。
// 如果该流程已有正在运行的实例,cascade=false 会抛异常;cascade=true 会强制删除运行中数据
repositoryService.deleteDeployment(deploymentId, true);
5. 模型管理 (Model - 设计器专用)
这些方法主要用于操作 ACT_RE_MODEL 表,服务于前端流程设计器(保存草稿、发布草稿)。
5.1 创建/保存模型草稿
// 1. 创建元数据
Model model = repositoryService.newModel();
model.setName("请假流程草稿");
model.setKey("leave");
model.setMetaInfo("{\"description\":\"这是一个草稿\"}");
repositoryService.saveModel(model); // 插入 ACT_RE_MODEL
// 2. 保存 XML 内容(草稿源文件)
String editorSource = "{\"resourceId\":\"... JSON 格式的流程设计数据 ...\"}";
repositoryService.addModelEditorSource(model.getId(), editorSource.getBytes("UTF-8"));
5.2 将模型草稿部署为流程定义
这是将“设计”转化为“运行”的关键步骤。
// 1. 获取模型
Model modelData = repositoryService.getModel(modelId);
// 2. 获取源文件字节流
byte[] bytes = repositoryService.getModelEditorSource(modelData.getId());
// 3. 转换(假设 bytes 是 JSON 格式,需转为 BPMN XML,通常用 Jackson 或 Flowable 转换器)
// ... 转换逻辑省略,得到 bpmnBytes ...
// 4. 执行部署
Deployment deployment = repositoryService.createDeployment()
.name(modelData.getName())
.addString(modelData.getKey() + ".bpmn20.xml", new String(bpmnBytes))
.deploy();
RuntimeService
RuntimeService 是 Flowable 引擎中负责管理 “正在运行” 的流程实例和执行流的核心服务接口。它的生命周期覆盖了从流程启动(Start)到流程结束(End)之间的所有动态操作。
它主要操作以下几张核心数据库表:
- ACT_RU_EXECUTION:运行时执行实例表(核心指针)。
- ACT_RU_VARIABLE:运行时变量表。
- ACT_RU_IDENTITYLINK:运行时用户关系表(如发起人、参与者)。
以下是 RuntimeService 最核心的方法分类及代码实例:
1. 启动流程实例 (Start Process)
这是 RuntimeService 最基础的功能,用于根据流程定义(Definition)创建一个新的运行实例(Instance)。
1.1 通过 Key 启动(最常用)
使用流程定义的 key 启动,引擎会自动选择最新版本的流程定义。
// 1. 准备启动时的全局变量
Map<String, Object> variables = new HashMap<>();
variables.put("applicant", "zhangsan");
variables.put("days", 3);
// 2. 启动流程
// 参数1: processDefinitionKey (XML中的 id="leave")
// 参数2: businessKey (业务主键,如请假单的数据库ID "10086"),方便后期关联查询
// 参数3: variables (初始变量)
ProcessInstance instance = runtimeService.startProcessInstanceByKey("leave", "10086", variables);
System.out.println("流程实例ID: " + instance.getId());
System.out.println("流程定义ID: " + instance.getProcessDefinitionId());
- 数据库影响:向
ACT_RU_EXECUTION插入执行流数据,向ACT_RU_VARIABLE插入变量数据。
1.2 指定发起人启动(Authentication)
Flowable 不会自动记录是谁发起的流程,需要显式设置。
// 设置当前线程的发起人ID
identityService.setAuthenticatedUserId("user_001");
// 启动流程
ProcessInstance instance = runtimeService.startProcessInstanceByKey("leave");
// 启动后通过 IdentityLink 表可以查到 starter 是 user_001
2. 查询执行实例 (Query Execution)
用于查询正在运行的流程实例 (ProcessInstance) 或执行流指针 (Execution)。
2.1 查询单个流程实例状态
// 根据流程实例ID查询
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId("25001")
.singleResult();
if (processInstance != null) {
System.out.println("当前节点: " + processInstance.getActivityId()); // 获取当前停留的节点ID
System.out.println("是否挂起: " + processInstance.isSuspended());
} else {
System.out.println("流程已结束或不存在");
}
2.2 关联业务Key查询
// 根据业务ID反查流程实例
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processDefinitionKey("leave")
.processInstanceBusinessKey("10086") // 业务表主键
.singleResult();
2.3 查询活跃的子执行流
在并行网关或多实例任务中,会有多个活跃的 Execution。
List<Execution> executions = runtimeService.createExecutionQuery()
.processInstanceId("25001")
.onlyChildExecutions() // 不查根节点,只查子节点
.list();
for (Execution exe : executions) {
System.out.println("执行流ID: " + exe.getId() + ", 当前停留节点: " + exe.getActivityId());
}
3. 变量管理 (Variables)
在流程运行过程中,动态读取或修改全局变量。
3.1 设置/更新变量
// 设置单个变量(如果存在则更新,不存在则创建)
runtimeService.setVariable("25001", "managerCheckResult", true);
// 批量设置变量
Map<String, Object> newVars = new HashMap<>();
newVars.put("auditTime", new Date());
newVars.put("score", 95);
runtimeService.setVariables("25001", newVars);
- 注意:这里操作的是
ACT_RU_VARIABLE表。
3.2 获取变量
// 获取所有变量
Map<String, Object> allVars = runtimeService.getVariables("25001");
// 获取指定变量
Integer days = (Integer) runtimeService.getVariable("25001", "days");
3.3 本地变量 (Local Variables)
setVariableLocal 设置的变量只绑定在当前的执行流(Execution)上,通常用于多实例子流程或特定分支的数据隔离。
// executionId 是子执行流ID,不是流程实例ID
runtimeService.setVariableLocal(executionId, "loopCounter", 1);
4. 流程控制 (Control)
对正在运行的流程进行干预,如强制删除、挂起、跳转。
4.1 删除流程实例
强制终止一个流程,通常用于“作废”操作。
// 参数1: processInstanceId
// 参数2: deleteReason (删除原因,会记录在历史表中)
runtimeService.deleteProcessInstance("25001", "用户主动撤销申请");
- 数据库影响:删除
ACT_RU_*所有相关数据,更新ACT_HI_PROCINST的DELETE_REASON_。
4.2 挂起/激活流程实例
挂起后,该实例无法继续流转(任务无法完成)。
// 挂起
runtimeService.suspendProcessInstanceById("25001");
// 激活
runtimeService.activateProcessInstanceById("25001");
4.3 节点跳转 (Change State)
这是 Flowable 强大的**“自由跳转”**功能,常用于实现复杂的“驳回”、“任意退回”逻辑。
// 将流程从“经理审批”节点(Activity_Manager) 强行移动到 “填写申请”节点(Activity_Apply)
runtimeService.createChangeActivityStateBuilder()
.processInstanceId("25001")
.moveActivityIdTo("Activity_Manager", "Activity_Apply")
.changeState();
- 逻辑:引擎会计算并销毁当前节点的所有 Task 和 Execution,然后在目标节点重新生成新的 Execution 和 Task。
5. 信号与消息触发 (Signal & Message)
用于处理中间捕获事件(Intermediate Catch Event),实现流程间的通信或外部触发。
5.1 触发信号事件
// 广播信号:所有订阅了 "alertSignal" 的流程实例都会收到并继续向下执行
runtimeService.signalEventReceived("alertSignal");
// 向指定执行流发送信号
runtimeService.signalEventReceived("alertSignal", executionId);
5.2 触发消息事件
消息通常是点对点的。
// 触发在这个执行流上等待的消息捕获事件
runtimeService.messageEventReceived("paymentSuccessMessage", executionId);
// 也可以通过消息名启动一个通过消息启动的流程
runtimeService.startProcessInstanceByMessage("startMsg");
TaskService
TaskService 是 Flowable 引擎中专门用于管理 “人工任务” (UserTask) 的服务接口。它的核心职责是处理所有需要人工干预的环节,包括任务的查询、领取、办理、转派以及任务相关的附属信息(如评论)管理。
它主要操作以下核心数据库表:
- ACT_RU_TASK:运行时任务表(最核心,记录当前待办)。
- ACT_RU_IDENTITYLINK:任务与人员的关系表(记录候选人、候选组)。
- ACT_RU_VARIABLE:任务级别的局部变量。
- ACT_HI_COMMENT:任务评论表(审计用)。
以下是 TaskService 最常见的方法分类及代码实例:
1. 任务查询 (Querying Tasks)
这是构建“待办列表”和“公办池”的基础。
1.1 查询“我的待办” (Direct Assignee)
查询明确指派给某个用户的任务。
// 查询分配给 "zhangsan" 的所有未挂起任务,按创建时间倒序
List<Task> todoList = taskService.createTaskQuery()
.taskAssignee("zhangsan") // 指定办理人
.active() // 仅查询激活状态的任务
.orderByTaskCreateTime().desc()
.list(); // 执行查询
for (Task task : todoList) {
System.out.println("任务ID: " + task.getId());
System.out.println("任务名称: " + task.getName());
System.out.println("流程实例ID: " + task.getProcessInstanceId());
}
- SQL 逻辑:
SELECT * FROM ACT_RU_TASK WHERE ASSIGNEE_ = 'zhangsan' ...
1.2 查询“候选任务” (Candidate User/Group)
查询放入“公海池”的任务(即没有指定具体人,只指定了角色或部门,需要去“签收”的任务)。
// 查询 "finance" 组的成员可以看到的任务,或者直接指定给 "lisi" 作为候选人的任务
List<Task> poolList = taskService.createTaskQuery()
.taskCandidateGroup("finance") // 候选组:财务部
// .taskCandidateUser("lisi") // 或者候选人:李四
.list();
- 原理:需要关联查询
ACT_RU_IDENTITYLINK表。
1.3 综合查询(带业务Key和变量)
Task task = taskService.createTaskQuery()
.processDefinitionKey("leave") // 流程定义Key
.processInstanceBusinessKey("10086") // 业务主键
.includeProcessVariables() // 预加载流程变量(避免N+1查询)
.singleResult(); // 返回单个结果
2. 任务生命周期管理 (Lifecycle)
控制任务状态的流转。
2.1 签收任务 (Claim)
将一个“公海”任务(无 Assignee)锁定给自己。
String taskId = "5001";
String userId = "zhangsan";
// 签收任务
// 前提:该任务目前的 Assignee 必须为 null,且 userId 必须在 Candidate 列表中(除非是管理员)
taskService.claim(taskId, userId);
- 数据库影响:更新
ACT_RU_TASK表,将ASSIGNEE_字段设为"zhangsan"。
2.2 归还任务 (Unclaim)
不想办了,退回公海池。
// 将 Assignee 设为 null,任务重新变为对所有候选人可见
taskService.unclaim(taskId);
2.3 完成任务 (Complete) —— 最核心方法
提交任务,驱动流程向下走。
String taskId = "5001";
// 准备流转所需的变量(例如审批结果、下一步参数)
Map<String, Object> variables = new HashMap<>();
variables.put("approved", true);
variables.put("managerComment", "同意,情况属实");
// 完成任务,并传入全局变量
// 引擎会自动删除当前任务,计算下一步节点
taskService.complete(taskId, variables);
- 数据库影响:删除
ACT_RU_TASK当前记录,更新ACT_HI_TASKINST历史记录,驱动Execution向下流转。
2.4 完成任务并设置局部变量 (Local Scope)
如果你希望变量只在当前任务历史里保留,不污染全局流程变量。
// 参数3 true 代表 localScope
// 这些变量在任务完成后,Runtime 表中会消失,但能在 HistoryService 查到详情
taskService.complete(taskId, variables, true);
3. 任务委派与移交 (Delegation & Assignment)
处理“转办”和“协办”场景。
3.1 移交任务 (Set Assignee / Transfer)
直接把任务甩给别人,自己不再负责。
// 将任务直接转给 "lisi"
// 类似于“改派”
taskService.setAssignee(taskId, "lisi");
3.2 委派任务 (Delegate)
A 委派给 B。B 办理完后,任务会自动回到 A 手里(通常用于咨询意见)。
// 1. 张三委派给李四
taskService.delegateTask(taskId, "lisi");
// 此时任务状态:OWNER_ = zhangsan, ASSIGNEE_ = lisi, DELEGATION_State = PENDING
// ... 李四登录系统 ...
// 2. 李四办理任务(注意:委派任务不能用 complete,要用 resolve)
// 李四做完工作,调用 resolveTask 归还给张三
taskService.resolveTask(taskId);
// 此时任务状态:ASSIGNEE_ 变回 zhangsan, DELEGATION_State = RESOLVED
// ... 张三重新看到任务 ...
// 3. 张三最终提交
taskService.complete(taskId);
4. 任务数据管理 (Data & Variables)
4.1 设置/获取任务变量
// 设置任务局部变量(Local Variable)
// 这些数据绑定在 TaskID 上,而不是 ProcessInstanceID 上
taskService.setVariableLocal(taskId, "localNote", "这是一个临时备注");
// 获取变量
Object val = taskService.getVariableLocal(taskId, "localNote");
4.2 添加审批意见 (Comment)
这是 Flowable 提供的标准审批意见存储方式。
// 参数:taskId, processInstanceId, message
taskService.addComment(taskId, processInstanceId, "同意,请尽快处理");
// 查询某任务的评论
List<Comment> comments = taskService.getTaskComments(taskId);
- 数据库影响:插入
ACT_HI_COMMENT表。
4.3 管理附件 (Attachment)
// 参数:attachmentType, taskId, processInstanceId, attachmentName, attachmentDescription, url
taskService.createAttachment("url", taskId, processInstanceId, "发票扫描件", "电子发票", "http://oss.aliyun.../file.pdf");
5. 独立任务管理 (Standalone Task)
创建一个不属于任何流程实例的“孤立任务”。(场景较少,用于简单的任务看板)。
// 1. 新建任务
Task newTask = taskService.newTask();
newTask.setName("临时会议任务");
newTask.setAssignee("zhangsan");
// 2. 保存入库
taskService.saveTask(newTask);
// 3. 删除任务(物理删除)
// 如果是流程中的任务,deleteTask 会报错,必须用 deleteProcessInstance
// 如果是 cascade=true,则连同历史一起删
taskService.deleteTask(newTask.getId(), true);
HistoryService
HistoryService 是 Flowable 引擎中专门用于查询历史数据(即已经发生过的数据)的服务接口。
与 RuntimeService(只查正在运行的数据)不同,HistoryService 查询的是 ACT_HI_* 系列表。无论流程是正在运行还是已经结束,只要配置了相应的历史级别(History Level),数据都会沉淀在这里。
它主要操作以下核心数据库表:
- ACT_HI_PROCINST:历史流程实例表(记录一次完整的业务流程)。
- ACT_HI_TASKINST:历史任务实例表(记录每一个人工任务的开始、结束、耗时)。
- ACT_HI_ACTINST:历史活动实例表(记录流程走过的每一个节点,包括网关、连线、开始/结束节点)。
- ACT_HI_VARINST:历史变量表(记录变量的最终值或每一次变化)。
以下是 HistoryService 最常见的方法分类及代码实例:
1. 历史流程实例查询 (Historic Process Instances)
用于查询“已经结束”的流程,或者查询某个用户发起的历史所有流程(包含进行中和已结束)。
1.1 查询“我发起的”所有流程
这是“我的申请记录”功能的后端实现基础。
String currentUserId = "zhangsan";
List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery()
.startedBy(currentUserId) // 过滤发起人
.finished() // (可选) 只查已结束的。如果不加,会包含正在运行的
.orderByProcessInstanceEndTime().desc() // 按结束时间倒序
.list();
for (HistoricProcessInstance hpi : list) {
System.out.println("流程ID: " + hpi.getId());
System.out.println("耗时(毫秒): " + hpi.getDurationInMillis());
System.out.println("开始时间: " + hpi.getStartTime());
System.out.println("结束时间: " + hpi.getEndTime());
System.out.println("删除原因: " + hpi.getDeleteReason()); // 如果是正常结束则为null,驳回/取消会有值
}
- SQL 映射:
SELECT * FROM ACT_HI_PROCINST WHERE START_USER_ID_ = 'zhangsan' ...
1.2 查询特定业务数据的历史流程
通过业务 Key(如订单号)反查流程历史。
HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery()
.processInstanceBusinessKey("ORDER_20260127")
.singleResult();
2. 历史任务查询 (Historic Task Instances)
用于查询“我已办的任务”(Done List)。这是工作流系统中非常高频的查询。
2.1 查询“我的已办”
查询某个用户曾经处理过的所有任务。
List<HistoricTaskInstance> doneList = historyService.createHistoricTaskInstanceQuery()
.taskAssignee("zhangsan") // 指定办理人
.finished() // 关键:必须查已完成的(finished)
.orderByHistoricTaskInstanceEndTime().desc()
.list();
for (HistoricTaskInstance hti : doneList) {
System.out.println("任务ID: " + hti.getId());
System.out.println("任务名称: " + hti.getName());
System.out.println("办理耗时: " + hti.getDurationInMillis()); // 领取到完成的时间差
System.out.println("处理时间: " + hti.getEndTime());
}
- SQL 映射:
SELECT * FROM ACT_HI_TASKINST WHERE ASSIGNEE_ = 'zhangsan' AND END_TIME_ IS NOT NULL ...
2.2 包含流程变量查询
为了避免 N+1 查询问题,可以在查历史任务时直接把当时的流程变量带出来。
List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
.taskAssignee("zhangsan")
.includeProcessVariables() // 联查变量表
.list();
// 获取变量
Map<String, Object> vars = list.get(0).getProcessVariables();
3. 历史活动查询 (Historic Activity Instances)
这是实现 “流程进度图” 或 “审批轨迹” 的核心方法。它能查出流程具体走过了哪些节点(包括连线、网关、自动服务任务等)。
3.1 查询流程流转轨迹
String processInstanceId = "25001";
List<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceStartTime().asc() // 按时间正序,还原发生顺序
.list();
for (HistoricActivityInstance hai : activities) {
System.out.println("节点ID: " + hai.getActivityId());
System.out.println("节点名称: " + hai.getActivityName());
System.out.println("节点类型: " + hai.getActivityType()); // userTask, startEvent, exclusiveGateway
System.out.println("处理人: " + hai.getAssignee()); // 如果是 UserTask 会有人
}
- 数据用途:前端拿到这个 List 后,可以将 BPMN 流程图中对应的节点标红,展示流程走到了哪一步,以及经过了哪些路径。
4. 历史变量查询 (Historic Variable Instances)
用于审计数据。例如:查询某个流程结束时,“请假天数”最终被改成了多少,或者查询某个变量的历史修改记录。
4.1 查询流程实例的所有历史变量
List<HistoricVariableInstance> vars = historyService.createHistoricVariableInstanceQuery()
.processInstanceId("25001")
.list();
for (HistoricVariableInstance var : vars) {
System.out.println("变量名: " + var.getVariableName());
System.out.println("变量值: " + var.getValue());
System.out.println("最后更新时间: " + var.getLastUpdatedTime());
}
4.2 查询特定变量
HistoricVariableInstance scoreVar = historyService.createHistoricVariableInstanceQuery()
.processInstanceId("25001")
.variableName("score")
.singleResult();
5. 历史清理与删除 (Delete History)
当历史数据量过大(如千万级),或者业务要求“撤销”某个归档流程时,需要删除历史数据。
5.1 删除单个历史流程实例
这将级联删除该流程实例下的所有历史任务、历史变量、历史活动记录。
// 参数: processInstanceId
historyService.deleteHistoricProcessInstance("25001");
- SQL 影响:从
ACT_HI_PROCINST,ACT_HI_TASKINST,ACT_HI_VARINST等表中物理删除相关行。
5.2 删除历史任务
通常不推荐单独删除任务,容易导致数据不一致。
historyService.deleteHistoricTaskInstance("taskId_1001");
6. 高级功能:本机查询 (Native Query)
当 Flowable 提供的 API 无法满足复杂的报表需求(例如复杂的 JOIN 或统计查询)时,可以使用 Native Query 直接写 SQL,但返回的是 Flowable 的实体对象。
// 统计每个用户办理了多少个任务
String sql = "SELECT count(*) FROM " + managementService.getTableName(HistoricTaskInstance.class) +
" T WHERE T.ASSIGNEE_ = #{assignee}";
long count = historyService.createNativeHistoricTaskInstanceQuery()
.sql(sql)
.parameter("assignee", "zhangsan")
.count();
第四章,Flowable实例流程
一,管理员创建流程分类
在正式设计复杂的业务流程之前,我们需要先为它们建立一个“文件夹体系”。流程分类(Category)的作用就是对不同业务域(如人事、财务、行政)的流程进行逻辑隔离与归纳。
1. 业务交互流程
这一步是整个 BPM 系统的入口。管理员的操作路径非常直观:
- 操作入口:登录后台管理系统,进入【工作流】->【流程分类】菜单。
- 输入信息:点击“新增”按钮,填写分类的基础元数据。
- 分类名称 (
name):展示给用户看的名字,例如Test03(或“人事考勤”)。 - 分类编码 (
code):系统内部识别的唯一标识,例如Test03(或hr_attendance)。 - 排序 (
sort):决定该分类在列表中的显示顺序,例如3。
2. 前后端数据流转
当管理员点击“确定”后,数据在系统内部经历了一次标准的 CRUD:
- 前端请求 (Request):
前端封装表单数据,发起POST请求至/bpm/category/create接口。
// 请求载荷示例
{
"name": "Test03",
"code": "Test03",
"sort": 3
}
-
后端处理 (Processing):
Controller 层接收请求后,下沉至 Service 层进行核心校验。 -
唯一性检查:这是最关键的业务规则。系统会查询
bpm_category表,确保传入的name和code在全表中是唯一的。如果已存在,直接抛出异常,防止数据冲突。 -
持久化:校验通过后,后端将数据封装为 Entity 对象。
-
数据库落地 (Database):
最终,一条新的记录被插入到业务表bpm_category中。
注意:bpm_category是 业务扩展表,而非 Flowable 的原生表。Flowable 虽然也有 Category 字段,但为了实现更复杂的层级管理和元数据控制,我们通常维护一张独立的分类表。
二,管理员创建流程表单
在定义了流程分类后,下一步是构建流程流转中承载数据的容器——流程表单。本环节通过低代码形式,将业务需求转化为系统可识别的数据结构。
1. 可视化表单设计
管理员进入后台管理系统,通过内置的表单设计器进行可视化配置。在此场景中,管理员创建了一个名为“请假表单”的业务单据,并拖拽配置了两个核心组件:
- 请假原因:单行文本输入框。
- 请假时间:单行文本输入框(用于输入具体天数)。
完成布局与属性配置后,点击保存触发提交。
2. 前后端交互协议
前端将可视化的配置结果转化为 JSON 数据结构,并通过 POST 请求发送至 /bpm/form/create 接口。请求载荷包含三个核心参数:
-
name(表单名称):
标识表单的业务名称,此处为"请假表单"。 -
conf(全局配置):
定义表单的整体样式与行为。- 样式参数:如
"labelPosition": "right"(标签右对齐)、"labelWidth": "100px"(标签宽度)。 - 按钮控制:定义了
"submitBtn"(提交按钮)显示,而"resetBtn"(重置按钮)隐藏。
- 样式参数:如
-
fields(组件字段定义):
这是表单的核心数据模型,以数组形式定义了具体控件的属性。- 组件一(请假原因):字段标识为
field: "Fkkgmkfa5l8eabc",类型为input,标题为title: "请假原因"。 - 组件二(请假时间):字段标识为
field: "Fl6ymkfbx7rrahc",类型为input,标题为title: "请假时间"。
- 组件一(请假原因):字段标识为
注意:前端生成的随机字符串 ID(如
Fkkg...)是后续流程运行时存取具体业务值的唯一键(Key)。
3. 数据持久化存储
后端接收请求后,解析并校验参数,随后执行插入操作:
-
目标表:
bpm_form(业务表单定义表)。 -
存储逻辑:
conf和fields字段被序列化为 JSON 字符串 存储在对应的数据库列中。- 系统为该表单生成一个全局唯一的
id。
-
返回结果:返回创建成功状态。
此步骤生成的 formId 至关重要,它将在下一章“创建流程模型”时被引用,从而实现“流程定义”与“业务表单”的物理绑定。
三,管理员创建流程模型
在完成了分类与表单的创建后,核心环节便是构建流程模型。此步骤旨在定义业务流转的逻辑规则(BPMN 图),并将前两章创建的分类与表单进行物理绑定。
1. 模型配置与可视化设计
管理员在后台管理系统中新建流程模型,操作分为两个维度:
-
元数据配置:
- 基本信息:定义流程名称为
请假流程,唯一标识 Key 设为leave,并归属到Test03分类下。 - 表单绑定:选择上一章创建的“请假表单”,系统在后端记录该表单的唯一标识
formId=41。
- 基本信息:定义流程名称为
-
BPMN 图形设计:
- 进入流程设计器(Modeler),绘制标准的 BPMN 2.0 流程图。
- 配置起始节点(StartEvent),确保其关联了上述请假表单(用于发起流程时渲染)。
- 绘制结束节点(EndEvent)以构成闭环,并点击保存。
2. 数据传输协议
前端将配置元数据与图形数据合并,通过 POST 请求发送至 /bpm/model/create 接口。核心载荷(Payload)包含:
- 元数据:
name=“请假流程”,key=“leave”,category=“Test03”,formId=41。 - 图形数据:
bpmnXml(由设计器生成的标准 XML 字符串)。
3. 后端处理与双表存储
后端接收请求后,首先校验模型 Key 的合法性及唯一性。校验通过后,通过 Flowable 原生的 RepositoryService 执行两步核心存储操作,分别涉及模型元数据表与通用字节资源表:
-
步骤一:存储模型元数据 (
ACT_RE_MODEL)
调用repositoryService.saveModel(model)初始化模型记录。KEY_/NAME_/CATEGORY_:直接映射传入的基本信息。META_INFO_(关键扩展):由于 Flowable 原生模型表没有formId字段,框架将formId: 41以及描述信息封装为 JSON 字符串,存储在此字段中。这是实现“流程-表单”绑定的关键。
-
步骤二:存储模型源文件 (
ACT_GE_BYTEARRAY)
调用repositoryService.addModelEditorSource(id, bytes)处理 BPMN XML 数据。- 数据转换:将前端传来的
bpmnXml字符串转换为UTF-8字节流。 - 持久化:将字节流作为一条记录插入
ACT_GE_BYTEARRAY表。此表专门用于存储二进制大对象(BLOB)。 - 建立关联:Flowable 自动将新生成的字节记录 ID 回填至
ACT_RE_MODEL表的EDITOR_SOURCE_VALUE_ID_字段。
- 数据转换:将前端传来的
四,版本化与发布——管理员部署流程模型
流程模型在“部署”之前,仅仅是存储在数据库中的静态设计草稿。部署操作是将设计草稿转化为引擎可执行代码(ProcessDefinition)的关键步骤,同时涉及严格的版本控制与数据快照机制。
1. 部署触发与完整性校验
管理员在流程模型列表中选择“请假流程”,点击【发布】按钮。
- 接口交互:前端通过
POST请求调用/bpm/model/deploy接口,仅需传输模型主键id。 - 前置校验:后端根据
id查询ACT_RE_MODEL表获取模型元数据及 XML 源码。系统调用 Flowable 校验器(BPMN Validator)对 XML 进行语法分析,检查是否存在游离节点、缺失的起始/结束事件或不合法的连线逻辑。
2. 核心部署:Flowable 引擎的三表联动
校验通过后,后端调用 Flowable 原生接口 repositoryService.createDeployment()...deploy() 执行部署。这是一个原子性事务,底层数据库发生如下级联变化:
- 生成部署档案 (
ACT_RE_DEPLOYMENT):
插入一条新记录,生成全局唯一的DEPLOYMENT_ID_。该记录标识了本次发布的版本快照时间与名称。 - 固化资源文件 (
ACT_GE_BYTEARRAY):
引擎将模型草稿中的 XML 源码复制一份,作为不可变的资源文件插入此表,并将DEPLOYMENT_ID_关联到上一条记录。
技术修正:此处实现了“设计”与“运行”的物理隔离。即便后续管理员修改了模型草稿(Model),也不会影响已部署运行的资源文件。
- 生成流程定义 (
ACT_RE_PROCDEF):
引擎解析 XML 结构,提取流程规则,插入一条流程定义记录。 - 版本控制:引擎自动查询该 Key(
leave)的历史版本,将VERSION_字段设为“最大版本号 + 1”。 - 状态关联:该记录同时关联上述的
DEPLOYMENT_ID_和资源文件名称。
3. 扩展元数据存储 (bpm_process_definition_info)
由于 Flowable 原生的 ACT_RE_PROCDEF 表结构固定,无法存储业务框架特有的配置(如关联的 formId、自定义图标、描述等)。
系统在此步执行扩展逻辑:从 ACT_RE_MODEL 的 META_INFO_ 中提取表单 ID 等信息,结合新生成的 processDefinitionId,插入到自定义扩展表 bpm_process_definition_info 中。这确保了流程定义与业务表单在运行时的强绑定。
4. 模型状态回写
部署的最后一步是更新设计草稿的状态。系统将新生成的 deploymentId 回写到 ACT_RE_MODEL 表的 DEPLOYMENT_ID_ 字段。
- 字段非空:表示该模型已发布,且指向最新的部署版本。
- 逻辑闭环:至此,流程模型完成了从“设计态”到“运行态”的转化,具备了发起流程实例的能力。
5. 深入解析:设计态与运行态的资源隔离(ACT_GE_BYTEARRAY 实例分析)
在执行部署操作后,若观察底层的通用字节资源表 ACT_GE_BYTEARRAY,会发现同一个“请假流程”存在两类截然不同的 XML 资源记录。这并非数据冗余,而是 Flowable 架构中为了实现“设计态”与“运行态”物理隔离而设计的核心机制。
我们将第三章(创建模型)与第四章(部署模型)产生的两条关键记录进行对比分析(参考数据库实例数据):
第一类记录:设计态源文件(Editor Source)
-
来源:产生于第三章“创建流程模型”,对应
repositoryService.saveModel操作。 -
实例特征:
NAME_:固定为source。这是 Flowable 设计器专用的标识,代表这是“设计草稿”。DEPLOYMENT_ID_:(Null)。关键特征。因为它是草稿,尚未发布,所以不关联任何部署记录。REV_:7(示例值)。高版本号表明管理员在设计器中点击了 7 次保存。
-
技术含义:这条记录是可变的。它仅供前端流程设计器(Model Editor)回显和编辑使用。无论管理员如何修改这条记录,都不会影响线上正在运行的业务。
第二类记录:运行态定义文件(Deployment Resource)
-
来源:产生于第四章“部署流程模型”,对应
repositoryService.deploy操作。 -
实例特征:
NAME_:变更为具体的资源名称leave.bpmn(格式通常为流程Key+.bpmn)。DEPLOYMENT_ID_:7e7559db...(示例值)。有具体值,指向ACT_RE_DEPLOYMENT表的一条部署记录。REV_:1。永远为 1。
-
技术含义:这条记录是不可变的快照。部署动作发生时,引擎读取了上述的
source草稿,将其转化为标准的 BPMN 2.0 XML 格式,并复制一份作为永久归档插入此表。流程引擎(Process Engine)在流转任务时,只读取这条记录,完全忽略source记录。
五,实例初始化——员工发起流程
在流程模型部署上线后,业务流转的实质性阶段始于“流程实例(Process Instance)”的创建。本章阐述员工提交申请时,系统如何将静态的流程定义转化为动态的运行数据。
1. 业务交互与数据封装
员工在后台管理系统前端访问已部署的“请假流程”入口。
- 表单渲染:前端根据第二章定义的 JSON 配置渲染动态表单,员工填写业务数据:
请假原因=“看病”,请假时间=“3”。 - 提交动作:点击“发起”按钮,触发提交逻辑。
2. 接口协议与载荷定义
前端封装请求参数,发送 POST 请求至 /bpm/process-instance/create。请求载荷(Payload)包含两个核心要素:
processDefinitionId:指向具体的流程定义版本 ID(如leave:1:7e82...),确保基于正确的版本发起。variables:将表单业务数据封装为键值对 Map。
{
"Fkkgmkfa...": "看病", // Key为表单设计时生成的随机字段ID
"Fl6ymkfb...": "3"
}
3. 后端预处理(业务层)
后端 Service 层接收请求后,在调用引擎之前执行一系列业务校验与数据增强:
- 定义校验:根据
processDefinitionId查询ACT_RE_PROCDEF表,确认流程定义处于激活(Active)状态。 - 权限与合法性校验:验证发起用户是否具备该流程的发起权限,并校验传入参数的格式合法性。
- 上下文增强:向
variables中注入系统级内置变量,例如:PROCESS_START_USER_ID: 发起人 ID。PROCESS_STATUS: 初始化为“审批中”。
4. 引擎核心执行(持久化层)
业务层构建 ProcessInstanceBuilder 并调用 .start() 方法。此操作是一个原子性事务,触发 Flowable 引擎执行节点流转与多表级联写入。
关键数据库操作:
-
A. 初始化执行流 (
ACT_RU_EXECUTION)
引擎并非只插入一条记录,通常会生成两条记录以构建执行树:- 根执行流 (Root Execution):代表流程实例本身。
ID_与PROC_INST_ID_相同,PARENT_ID_为空。 - 子执行流 (Child Execution):代表当前的执行指针。
PARENT_ID_指向根执行流 ID,ACT_ID_指向当前停留的节点(如第一个用户任务)。
- 根执行流 (Root Execution):代表流程实例本身。
-
B. 变量持久化 (
ACT_RU_VARIABLE)
将增强后的variablesMap 扁平化存储。每一条键值对对应表中一行记录,外键PROC_INST_ID_关联上述的根执行流 ID。 -
C. 身份链路绑定 (
ACT_RU_IDENTITYLINK)
引擎会向此表插入一条记录,标记当前用户为流程的starter(发起人)。这对于后续查询“我发起的流程”至关重要。 -
D. 任务生成 (
ACT_RU_TASK)
引擎计算流程图路径,从StartEvent流转至第一个节点(假设为用户任务)。- 在表中插入一条新任务记录。
ASSIGNEE_(办理人):根据流程图配置(如${initiator}或固定值)解析并填入具体用户 ID。EXECUTION_ID_:关联到上述的子执行流 ID(注意:是关联子执行流,而非根实例)。
- 在表中插入一条新任务记录。
-
E. 历史审计同步 (
ACT_HI_*)start()不仅操作运行时表(Runtime),会同步写入历史表以保证审计可追溯:ACT_HI_PROCINST:记录流程实例开始时间。ACT_HI_ACTINST:记录“开始节点”已完成,“用户任务节点”已开始。ACT_HI_TASKINST:记录生成的第一个任务的历史存根。ACT_HI_VARINST:记录变量的历史快照。
start() 操作标志着流程实例的正式诞生。此时,数据库中形成了完整的运行时数据结构(Execution Tree + Variables + Task),且首个审批节点的办理人已能在其“待办任务”列表中查询到该记录。
六,领导查看待办列表
流程实例发起后,任务流转至审批节点。此时,系统需要精准地将“任务”分发到对应审批人的工作台中。本章阐述系统如何通过身份上下文,从引擎中检索出属于当前用户的活跃任务。
1. 交互入口与请求上下文
领导一登录后台管理系统,进入【我的待办】菜单。
- 接口协议:前端发起
GET请求至/bpm/task/todo-page接口。 - 身份凭证:请求头(Header)中携带了
Authorization: Bearer {token}。这是后端识别“当前是谁在查任务”的唯一凭证,避免了在 URL 参数中显式传递 UserID 带来的安全隐患。
2. 后端上下文解析
后端接口接收请求后,并非直接调用引擎,而是先进行上下文处理:
- 身份提取:通过安全框架(如 Spring Security)解析 Token,从
SecurityContextHolder中获取当前登录用户的 ID(例如User_Leader)。 - 分页参数:接收前端传递的分页参数(如
pageNo=1, pageSize=10),以便应对大量待办数据的场景。
3. 引擎查询与底层 SQL 构建
后端 Service 层构建 Flowable 的 TaskQuery 对象,执行核心查询逻辑。代码层面的 taskService.createTaskQuery()... 会被引擎翻译为复杂的 SQL 语句,涉及对 运行时任务表 (ACT_RU_TASK) 和 运行时变量表 (ACT_RU_VARIABLE) 的联合操作。
关键查询链式调用如下:
-
taskAssignee(userId):- SQL 映射:
WHERE ASSIGNEE_ = 'User_Leader'。 - 含义:精准匹配分配给当前用户的任务。
- (注:实际业务中常用
taskCandidateOrAssigned以涵盖“候选人”模式,但此处按您描述的直接指派模式,使用 assignee 即可)
- SQL 映射:
-
active():- SQL 映射:
AND SUSPENSION_STATE_ = 1。 - 含义:仅查询激活状态的任务,过滤掉被挂起(Suspended)的任务。
- SQL 映射:
-
includeProcessVariables():- SQL 映射:
LEFT JOIN ACT_RU_VARIABLE ...。 - 含义:这是一个关键的性能优化操作。它指示引擎在查询任务的同时,通过**预加载(Eager Loading)**的方式一次性拉取关联的流程变量(如请假原因、请假天数)。这避免了后续遍历任务列表时产生 “N+1” 次查询数据库的问题。
- SQL 映射:
-
orderByTaskCreateTime().desc():- SQL 映射:
ORDER BY CREATE_TIME_ DESC。 - 含义:按任务创建时间倒序排列,确保最新的待办显示在最前。
- SQL 映射:
4. 数据转换与响应
引擎返回查询结果(List<Task>)后,后端不会直接将 Flowable 的原生实体返回给前端(因为包含大量引擎内部字段),而是进行对象转换(DTO/VO):
- 基础映射:提取 Task ID, Name, CreateTime 等基础字段。
- 变量注入:从查询结果中提取
processVariables(如{"Fkkg...": "看病", "Fl6y...": "3"}),并将其映射为前端可读的业务字段。 - 流程定义关联:通常还会根据
PROC_DEF_ID_关联查询流程定义名称(“请假流程”),以便用户知道这是什么流程的任务。
最终,前端接收到包含业务数据的标准分页列表,渲染出领导看到的待办表格。
七,流转与驱动——领导审批待办任务
当领导对待办任务进行“通过”操作时,系统不仅要处理业务层面的审批意见与签名,还需驱动 Flowable 引擎完成核心的“节点跃迁”:销毁当前任务,移动执行指针,并生成下一个任务节点。
1. 审批动作与接口协议
领导在任务详情页点击【通过】按钮,若业务场景配置了签名或审批意见,需同时提交相关数据。
- 接口调用:前端发送
PUT请求至/bpm/task/approve。 - 载荷参数:
id: 任务唯一标识(如d7d3cb54-f212...)。reason: 审批意见(可选)。signatureImage: 电子签名图片(取决于配置)。nextAssignee: 指定的下一节点审批人(若流程配置为手动选择)。
2. 动态规则校验(基于 BPMN 扩展属性)
后端接收请求后,首先进行基础校验(任务是否存在、流程实例是否活跃)。随后,进入基于模型的动态校验逻辑:
- 获取节点定义:根据任务的
taskDefinitionKey,从内存缓存的BpmnModel中定位到对应的FlowElement对象。 - 解析扩展属性:读取该节点配置的
extensionElements,检查是否存在自定义标签signEnable。 - 强制校验:若
signEnable=true,系统强制检查请求参数中是否包含signatureImage。若缺失,直接抛出业务异常,拦截后续操作。这一机制实现了“配置驱动校验”,无需硬编码。
3. 上下文准备:预设下一审批人
在触发流转前,系统需解决“下一棒交给谁”的问题。
- 推理与设置:根据前端传递的
nextAssignee或业务规则,确定下一节点的受托人。 - 变量持久化:调用
runtimeService.setVariables(),将目标用户 ID 写入ACT_RU_VARIABLE表。 - 目的:Flowable 引擎在创建下一个任务时,会通过 UEL 表达式(如
${approver})读取此变量作为新任务的 Assignee。
4. 引擎核心流转 (taskService.complete)
校验与参数准备完成后,调用 taskService.complete(taskId)。这是一个高度封装的原子操作,底层数据库发生剧烈的级联变化:
-
A. 任务销毁与归档 (Current Task)
- 物理删除:执行
DELETE操作,从ACT_RU_TASK表中移除当前领导的待办记录。 - 历史审计:同步更新
ACT_HI_TASKINST表,标记该任务状态为completed,并记录END_TIME_(结束时间)与DURATION_(任务耗时)。
- 物理删除:执行
-
B. 路径计算与指针移动 (Navigation)
- 读取模型:引擎从 Deployment Cache 中读取流程定义模型(无需每次查询数据库 XML),根据当前节点的“出口连线”计算目标节点。
- 移动指针:更新
ACT_RU_EXECUTION表。 - 将当前执行流的
ACT_ID_字段由“领导审批节点 ID”更新为“下一个节点 ID”。 - (注:若遭遇并行网关,此处会涉及执行流的分裂或聚合操作)
-
C. 下一任务生成 (Next Task)
- 节点实例化:若目标节点仍为 UserTask,引擎向
ACT_RU_TASK表执行INSERT操作,生成一条新的待办记录。 - 属性填充:新记录的
TASK_DEF_KEY_指向新节点 ID,ASSIGNEE_字段根据第 3 步预设的变量自动填充。 - 历史同步:同时向
ACT_HI_TASKINST插入一条新记录(无结束时间),标志着下一环节审计追踪的开始。
- 节点实例化:若目标节点仍为 UserTask,引擎向
Q&A
Task表的记录是在什么时候被插入的,用户任务的插入机制是什么
这个问题触及到了 Flowable 引擎的“自动执行”与“事务一致性”机制。
一、 核心场景:从“人工审批”流转到“自动服务”
假设流程图是这样的:
【用户任务 A (经理审批)】 --> 【服务任务 B (发送邮件)】 --> 【用户任务 C (总监审批)】
1. 触发点
你调用了 taskService.complete(Task_A)。这一瞬间,数据库事务(Transaction)开启。
2. 步骤推演
第一步:处理 Task A(起点)
- 动作:销毁 Task A。
- DB 操作:
DELETE FROM ACT_RU_TASK WHERE ID_ = 'Task_A'。
第二步:遇到 Task B(系统节点)—— 关键时刻!
-
引擎判断:引擎发现下一个节点是
ServiceTask(系统节点)。 -
ACT_RU_TASK 表:完全无视。引擎不会向这张表插入任何记录。因为不需要人来处理,没必要贴“告示”。
-
代码执行:引擎会在当前的 Java 线程里,直接实例化你配置的
JavaDelegate类,并执行.execute(execution)方法。 -
比如:发送一封 SMTP 邮件,或者更新一下 ERP 系统的数据库。
-
ACT_RU_EXECUTION 表:
-
指针短暂地变为 Task B 的 ID,执行完代码后,立即跳向下一个节点。
-
(在数据库里你甚至很难抓拍到它停在 Task B 的状态,因为太快了)
第三步:遇到 Task C(终点)
- 引擎判断:发现 Task B 后面连着的是
UserTask(总监审批)。 - ACT_RU_TASK 表:插入新记录!
- DB 操作:
INSERT INTO ACT_RU_TASK (ID_, NAME_='总监审批', ...)。
第四步:事务提交
- Commit:上述所有操作(删 A -> 跑代码 -> 插 C),都在同一个数据库事务里完成。
二、 深入理解:原子性与“连带责任”
这里有一个非常重要的“原子性”概念,很多初学者容易踩坑。
因为系统节点(ServiceTask)是不停车的,所以“Task A 完成” + “Task B 执行” + “Task C 创建”这三件事,在默认情况下是绑定在同一个事务里的。
后果 1:一荣俱荣,一损俱损
如果 Task B(发送邮件)的代码报错了(比如空指针异常、网络超时):
-
回滚 (Rollback):整个事务回滚。
-
现象:
- Task C 没创建。
- Task B 没执行完。
- 最可怕的是:Task A 复活了!因为删除操作也回滚了。
-
用户视角:经理点击了“通过”,转圈圈,然后报错“系统异常”。刷新页面一看,任务还在自己名下,仿佛刚才什么都没发生。
后果 2:没有“中间状态”
你永远无法在数据库里查到一条“正在执行发送邮件”的任务记录。
ACT_RU_TASK表里查不到。ACT_RU_EXECUTION表里也基本查不到(因为它瞬间就过去了)。- 只有在
ACT_HI_ACTINST(历史活动表) 里,事后能查到它曾经来过(Start Time 和 End Time 几乎只有几毫秒之差)。
Task表的Assgnee是什么时候分配的,分配机制是什么
Assignee 的分配发生在 Task 记录插入的那一刻(即 INSERT 语句执行时),但值的来源可能在插入之前就已经准备好了。Assignee 的分配主要有三种情况,时间点略有不同:
情况 A:静态指定或表达式(最常见)
- 配置:BPMN 中写死
assignee="zhangsan"或使用表达式assignee="${manager}"。 - 分配时机:同步分配。
- 引擎在准备执行
INSERT ACT_RU_TASK之前,会先解析 XML 里的assignee属性。 - 如果是
${manager},引擎会去查变量表ACT_RU_VARIABLE找到 manager 的值。
- 引擎在准备执行
- 入库状态:数据库里该条记录一生成,
ASSIGNEE_字段就有值了。
情况 B:人工手动选择
-
配置:BPMN 中配置
assignee="${flow_assignee_UserTask_01}"(动态变量)。 -
前置动作:
- 在调用
taskService.complete()之前,你的代码先执行了runtimeService.setVariable(...),把“张三”存进了变量表。
- 在调用
-
分配时机:读取变量并分配。
- 引擎创建新 Task 时,读取这个变量,填入
ASSIGNEE_字段。
- 引擎创建新 Task 时,读取这个变量,填入
-
入库状态:数据库记录生成时,字段已有值。
情况 C:先入池,后签收(Claim 模式)
- 配置:BPMN 中没写 assignee,但配置了
candidateGroups="dept_finance"(候选组)。 - 分配时机:延迟分配。
- Step 1 (创建时):
INSERT ACT_RU_TASK时,ASSIGNEE_字段是 NULL。此时任务在“公海池”。 - Step 2 (签收时):用户点击“签收”,调用
taskService.claim(taskId, "lisi")。 - Step 3 (更新):此时触发
UPDATE ACT_RU_TASK SET ASSIGNEE_='lisi' ...。
- Step 1 (创建时):
流程定义表EXECUTION执行流的记录中(非根流程),ACT_ID_字段指向执行流中用户任务的TASK_DEF_KEY_。ACT_ID_的生命周期,它的创建修改销毁都是什么时候,是由什么操作触发的
以下是 ACT_ID_ 字段在数据库层面的完整生命周期(创建、修改、销毁)及其触发机制。我们将场景设定为:并行网关分支中的一个子执行流。
一、 创建 (Creation)
时机:当流程流转遇到“并发分叉点”(如并行网关 Parallel Gateway)时。
-
触发操作:
- 流程到达并行网关,引擎发现有多个分支。
- 引擎决定保留(或挂起)父执行流,并为每一个分支裂变(Fork)出新的子执行流。
-
数据库行为 (
INSERT):- 引擎向
ACT_RU_EXECUTION插入一条新记录。 ID_:生成新的 UUID(子执行流 ID)。PARENT_ID_:指向根执行流。ACT_ID_:初始化为该分支上第一个节点的 ID。
- 引擎向
示例:
网关后连接着 ID 为 audit_task 的用户任务。
- SQL:
INSERT INTO ACT_RU_EXECUTION (ID_, ACT_ID_, ...) VALUES ('child_1', 'audit_task', ...) - 此时,
ACT_ID_的值就是'audit_task'。
二、 修改 (Modification)
时机:当任务完成,流程“迈向下一站”时。
这是最关键的阶段。ACT_ID_ 的修改代表了流程进度的推进。
场景 A:从“用户任务 A”流转到“用户任务 B” (UserTask -> UserTask)
-
触发操作:
- 后端调用
taskService.complete(taskId)。
- 后端调用
-
内存计算:
- 引擎计算出当前节点的出口连线,找到目标节点(用户任务 B,ID 为
approve_task)。
- 引擎计算出当前节点的出口连线,找到目标节点(用户任务 B,ID 为
-
数据库行为 (
UPDATE):- 引擎不会删除当前的执行流,而是直接修改它的指针位置。
- SQL:
UPDATE ACT_RU_EXECUTION SET ACT_ID_ = 'approve_task' WHERE ID_ = 'child_1'
注意:ACT_ID_ 的值永远指向“当前停留”的节点。一旦任务完成,它就指向下一个了。
场景 B:穿过“自动服务节点” (UserTask -> ServiceTask -> UserTask)
这是一个特殊情况。
-
触发操作:
taskService.complete(taskA)。 -
过程:
- 引擎先将内存中的
actId更新为 ServiceTask 的 ID。 - 但不提交事务(因为 ServiceTask 不停车)。
- 立即执行 Java 代码。
- 执行完后,内存中的
actId再次更新为下一个 UserTask C 的 ID。
- 引擎先将内存中的
-
数据库行为:
- 在最终提交事务时,数据库里的
ACT_ID_会直接从 A 跳变为 C。 - 现象:你几乎无法在数据库
ACT_RU_EXECUTION表中看到ACT_ID_指向 ServiceTask,因为它发生得太快且未持久化停车。
- 在最终提交事务时,数据库里的
三、 销毁 (Destruction)
时机:当当前分支走完,或者流程结束时。
场景 A:分支汇聚 (Join)
当子执行流到达并行网关的汇聚点 (Join Gateway)。
-
触发操作:
- 分支上的最后一个任务完成
taskService.complete()。 - 流程到达汇聚网关。
- 分支上的最后一个任务完成
-
数据库行为 (
DELETE):- 引擎判断:所有分支都到了吗?
- 如果没到齐:当前子执行流会在网关处“停车”等待(
ACT_ID_更新为网关 ID),或者被挂起。 - 如果到齐了:引擎会合并执行流。通常会保留一个执行流作为主干,删除其他多余的子执行流。
- 如果没到齐:当前子执行流会在网关处“停车”等待(
- SQL:
DELETE FROM ACT_RU_EXECUTION WHERE ID_ = 'child_1'
- 引擎判断:所有分支都到了吗?
场景 B:流程结束 (EndEvent)
当执行流到达结束节点 (EndEvent)。
-
触发操作:
- 最后一个节点的任务完成。
-
数据库行为 (
DELETE):- SQL:
DELETE FROM ACT_RU_EXECUTION WHERE ID_ = 'child_1' - 如果是根流程结束,连同根执行流一起删除。
- SQL:
在EXECUTION表中两个字段非常像:ID_和PROC_INST_ID_,在ACT_RU_VARIABLE表中也有EXECUTION_ID_,PROC_INST_ID_两个字段。EXECUTION不就是执行流实例么,为什么要分为两个字段,他们的区别和联系是什么
一、 核心概念区分
1. PROC_INST_ID_ (Process Instance ID)
- 含义:流程实例 ID。
- 地位:它是**“根”**。代表了这一次业务请求(比如“张三的请假单”)。
- 特性:无论流程内部怎么分叉、怎么循环,只要在这个单子里,所有产生的执行流记录,它们的
PROC_INST_ID_永远相同。 - 作用:用来标识“这是哪一单业务”。
2. ID_ (Execution ID)
- 含义:执行流 ID(主键)。
- 地位:它是**“枝”(也包含根)。代表了引擎当前的“并发指针”**。
- 特性:
- 如果是单线流程,只有一根主干,那么
ID_=PROC_INST_ID_。 - 如果遇到并行网关,流程会裂变出多个“子执行流”。此时,子执行流会有自己独立的
ID_,但它们的PROC_INST_ID_依然指向同一个根。
二、 实例演示: 并行
流程图:开始 -> 并行网关 -> (分支1: 经理审批 / 分支2: 总监审批) -> 汇聚
当流程运行到并行网关,发生分叉时,ACT_RU_EXECUTION 表里会同时存在 3条记录:
| 记录类型 | ID_ (主键) | PROC_INST_ID_ (归属) | PARENT_ID_ (父级) | 含义 |
|---|---|---|---|---|
| 根执行流 | 1001 | 1001 | NULL | 流程实例本身。它此时处于“等待/Scope”状态,作为容器管理全局变量。 |
| 子执行流 A | 1002 | 1001 | 1001 | 分支1的指针。正停在“经理审批”节点。 |
| 子执行流 B | 1003 | 1001 | 1001 | 分支2的指针。正停在“总监审批”节点。 |
结论:
PROC_INST_ID_(1001):告诉我们这三条记录都属于“张三的请假单”。ID_(1002/1003):告诉我们当前有两个并行的任务在处理,互不干扰。
三、 为什么 ACT_RU_VARIABLE 表也要有两个字段?
在变量表中,EXECUTION_ID_ 和 PROC_INST_ID_ 的区别对应了 “变量的作用域 (Scope)”。
1. 全局变量 (Global Variable)
-
场景:请假天数、请假原因。所有节点都能看到。
-
存储方式:
PROC_INST_ID_: 1001EXECUTION_ID_: 1001 (指向根执行流)
-
含义:这个变量绑在主干上,所有树枝都能读到。
2. 局部变量 (Local Variable)
-
场景:
- 多实例会签:在这个分支里,
approver是张三;在那个分支里,approver是李四。变量名一样,但值不同,且互不干扰。 - 并行分支:分支 A 需要一个临时变量
temp_score,分支 B 不需要。
- 多实例会签:在这个分支里,
-
存储方式:
PROC_INST_ID_: 1001 (依然属于这个单子)EXECUTION_ID_: 1002 (指向子执行流 A)
-
含义:这个变量只绑在“树枝 A”上。树枝 B(ID=1003)完全看不到它。
ACT_RU_VARIABLE表都存储什么,作用域是什么,变量的生命周期是什么
当流程实例在运行时,所有动态产生的业务数据(比如请假天数、审批结果、表单填写的内容、临时计算的标志位)都暂存在这张表里。
它是一个高度灵活的Key-Value(键值对)存储结构。
一、 它存什么?(数据结构与存储策略)
这张表的设计非常通用,为了能存下 Java 中的各种数据类型,它采用了一种“类型分发 + 多列存储”的策略。
1. 核心字段解析
NAME_: 变量名(Key)。比如"days","reason","approver".TYPE_: 变量类型。Flowable 支持多种类型,引擎根据这个字段决定去读哪一列的数据。string(字符串)integer/long/double(数字)boolean(布尔)date(日期)serializable(序列化对象,最复杂的一种)
2. 值存在哪里?(多列复用)
根据 TYPE_ 的不同,真正的值会被存放在不同的列中:
TEXT_: 存字符串(string)。LONG_: 存整数(integer,long)或日期的时间戳(date)或布尔值(0/1)。DOUBLE_: 存浮点数(double)。TEXT2_: 存长文本(JPA 实体等)。BYTEARRAY_ID_: [关键] 存二进制流 ID。- 如果你存的是一个 Java Object(比如
Person对象),引擎会把它序列化成字节流,存到ACT_GE_BYTEARRAY表中,然后在这里记录那个 ByteArray 的 ID。
二、 它的作用域 (Scope)
这张表里的数据并非都是“全局可见”的,它通过外键来实现数据隔离。
- 全局变量 (Global)
- 特征:
PROC_INST_ID_有值,TASK_ID_为空。 - 场景:请假天数、发起人姓名。
- 作用:整个流程实例,从头到尾,任何节点都能读写它。
- 本地/局部变量 (Local)
-
特征:
-
执行流局部:
EXECUTION_ID_指向某个子分支。只有该分支能看。 -
任务局部:
TASK_ID_有值。 -
场景:审批节点 A 的临时备注。
-
作用:一旦这个任务完成,或者分支合并,这个变量就消失了,不会污染全局环境。
三、 生命周期 (Lifecycle)
这是你最关心的部分。ACT_RU_VARIABLE 的生命周期与流程实例的生命周期强绑定。
1. 创建
变量是在流程运行过程中动态插入的。
-
场景 A:流程启动时
runtimeService.startProcessInstanceByKey("leave", variablesMap);- 动作:引擎在创建
EXECUTION后,立刻遍历 Map,执行INSERT INTO ACT_RU_VARIABLE。
- 动作:引擎在创建
-
场景 B:任务完成时
taskService.complete(taskId, variablesMap);- 动作:将 Map 里的数据插入或更新到表中。
-
场景 C:代码显式设置
runtimeService.setVariable(executionId, "score", 100);- 动作:即时插入。
2. 存活
只要流程还没结束,这些数据就一直存在于表中。
- 修改:如果你再次调用
setVariable("days", 5),引擎会执行UPDATE操作,覆盖旧值。 - 查询:当你调用
taskService.getVariables(taskId)时,引擎就是来查这张表。
3. 销毁
这是关键点:当流程结束(EndEvent)那一瞬间,这张表里的相关数据会被彻底清空!
- 触发:流程走到结束节点,或者被人工强制删除。
- 动作:
DELETE FROM ACT_RU_VARIABLE WHERE PROC_INST_ID_ = ... - 归宿:数据去哪了?
- 在删除之前,引擎会将它们搬家到历史表
ACT_HI_VARINST。 - Runtime 表 (RU):保持轻量级,只存正在跑的数据,跑完就删,保证查询快。
- History 表 (HI):存全量数据,用于日后审计(比如查“张三去年的请假单当时填了几天”)。
这是为您重写的博客段落。我们将抛弃截图,通过构建一个完整的“公司采购申请流程”业务场景,来模拟数据库中 ACT_RU_VARIABLE 表的真实数据形态。
四、 实例讲解
我们假设有一个 “IT设备采购流程”,当前运行到了 “技术经理审批” 节点。流程实例 ID 为 1001,当前任务 ID 为 Task-50。
此时,数据库表中会存在以下四类典型数据:
1. 业务表单数据(标准键值对)
这是用户在前端表单中显式填写的内容。引擎通常将其存储为简单的字符串或数字。
- 场景:员工申请购买一台电脑,价格 15000 元。
- 数据库记录:
| NAME_ (变量名) | TYPE_ (类型) | TEXT_ (文本值) | LONG_ (数值) |
|---|---|---|---|
| itemName | string | “MacBook Pro” | NULL |
| price | long | NULL | 15000 |
- 技术解析:
- 引擎根据
TYPE_字段决定去读哪一列。 string类型读取TEXT_列。integer或long类型读取LONG_列。这些数据是全局可见的,绑定在PROC_INST_ID_ = 1001上,后续的财务审批节点依然可以读取到价格进行校验。
- 引擎根据
2. 逻辑控制变量(系统隐式数据)
这类数据用户看不见,是后端为了控制流程走向而注入的“信号灯”。
- 场景:流程图中定义了一个排他网关:如果
price > 5000则走总监审批,否则直接走财务。 - 数据库记录:
| NAME_ | TYPE_ | TEXT_ | LONG_ |
|---|---|---|---|
| process_status | integer | NULL | 1 (审批中) |
| initiator | string | “zhangsan” | NULL |
- 技术解析:
process_status是开发人员为了方便查询而自定义的状态码。initiator是引擎自动记录的发起人,用于后续节点判断“谁发起的”。这些变量同样是全局范围。
3. 复杂对象数据(序列化存储)
当业务需要存储 Java 对象(如 List、Map 或自定义 POJO)时,这张表无法直接存下列,而是通过“外键”指向二进制表。
- 场景:采购申请中包含一个详细的配件清单(鼠标、键盘、支架),后端将其封装为
List<String> accessories对象。 - 数据库记录:
| NAME_ | TYPE_ | TEXT_ | BYTEARRAY_ID_ |
|---|---|---|---|
| accessories | serializable | NULL | Byte-999 |
- 技术解析:
TYPE_为serializable。- 引擎将 Java List 序列化为二进制流,存入
ACT_GE_BYTEARRAY表(ID为Byte-999)。 - 本表只存储引用 ID。当代码调用
getVariable("accessories")时,引擎会自动反序列化还原回 Java List 对象。
4. 任务局部变量(Local Scope)
这是最特殊的一类数据,它的生命周期极短,且作用域严格受限。
- 场景:技术经理在审批时,认为配置过高,给了一个临时评分
score=80,这个评分只属于当前审批环节,不需要传递给后续的财务。 - 数据库记录:
| NAME_ | TASK_ID_ | PROC_INST_ID_ | LONG_ |
|---|---|---|---|
| score | Task-50 | 1001 | 80 |
- 技术解析:
- 关键差异:注意
TASK_ID_字段有值(指向当前任务Task-50)。 - 而上述前三类数据的
TASK_ID_字段均为NULL。 - 这意味着变量
score仅在Task-50存活期间有效。一旦经理点击“完成”,这条记录会被立即物理删除,不会随流程流转。
- 关键差异:注意
在工作流flowable中,除了flowable自带的表之外,有些项目还定义了其它的扩展表:1.bpm_form:id,name,conf,fields…;2.bpm_process_definition_info:id,process_definition_id,model_id,form_conf,form_fields…。这些扩展表是怎么和flowable自带的表联系起来的,为什么要扩展表
ACT_RE_MODEL的META_INFO字段记录了流程模型对应的表单的formid,与bpm_form关联;bpm_process_definition_info通过process_definition_id,model_id来与ACT_RE_PROCDEF,ACT_RE_MODEL表关联。之所以要扩展这两个表,是因为原生的flowable的表单数据要写在META_INFO_里,且不支持表单业务与模型业务的解耦。因此yudao设计了一个bpm_form表来存储设计好的表单,并用META_INFO来关联表单ID。而flowable原生ACT_RU_EXECUTION也没有表单相关字段,因此扩展了bpm_process_definition_info表实现表单与流程定义的绑定
用户/管理员取消流程实例,后端发生了什么
第一步:标记流程实例状态 (Process Instance Level)
1. 代码逻辑
// 设置流程实例级别的状态为 CANCEL
runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS,
BpmProcessInstanceStatusEnum.CANCEL.getStatus());
// 设置流程实例级别的取消原因
runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason);
// 递归查找并处理子流程
List<ProcessInstance> childProcessInstances = runtimeService.createProcessInstanceQuery()
.superProcessInstanceId(id).list();
childProcessInstances.forEach(...) // 递归调用自己
2. 核心含义
这一步是给整个“案件”定性。不管是主流程还是子流程,都在它们的档案袋上盖一个红章:“已取消”。
3. 数据库操作
- 操作表:
ACT_RU_VARIABLE(运行时变量表) - 操作动作:INSERT (如果变量不存在) 或 UPDATE (如果变量已存在)
- 关键字段:
PROC_INST_ID_= 当前流程实例 ID (id)NAME_="bpm_process_instance_status"/"bpm_process_instance_reason"LONG_/TEXT_= 状态码 / 原因内容
- 注意:
TASK_ID_字段为 NULL (因为这是流程全局变量)。
第二步:标记运行时任务状态 (Task Level)
1. 代码逻辑
// 获取当前流程实例下所有“活着”的任务
List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
taskList.forEach(task -> {
// 防御性编程:如果任务已经是结束状态(并发场景可能发生),跳过
Integer otherTaskStatus = (Integer) task.getTaskLocalVariables().get(...);
if (BpmTaskStatusEnum.isEndStatus(otherTaskStatus)) return;
// 【核心】调用 processTaskCanceled,内部本质是 taskService.setVariableLocal
processTaskCanceled(task.getId());
});
2. 核心含义
这一步是给当前正在办理任务的“办事员”留遗言。
因为下一步我们就要强制杀死这些任务,如果不在这里把“取消原因”写进任务的变量里,等任务被删了,历史记录里就不知道这个任务为什么没做完就消失了。
3. 数据库操作
- 操作表:
ACT_RU_VARIABLE(运行时变量表) - 操作动作:INSERT
- 关键字段:
TASK_ID_= 当前循环到的任务 ID (task.getId()) —— 这是关键,通过此字段绑定到具体任务。NAME_="bpm_task_status"/"bpm_task_reason"LONG_/TEXT_=CANCEL(取消状态) /"系统自动取消..."
第三步:物理终止流程 (Engine Level)
1. 代码逻辑
// 1. 获取模型和结束节点
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(...);
EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel);
// 2. 获取当前所有停留节点的 Key (例如 ["UserTask_1", "UserTask_2"])
List<String> activityIds = ...;
// 3. 【核武器】执行跳转:把所有指针从当前位置,瞬间移到 EndEvent
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveActivityIdsToSingleActivityId(activityIds, endEvent.getId())
.changeState();
2. 核心含义
这是对引擎下达的“死刑命令”。
引擎会将执行流(Execution)强制跳转到结束节点。由于到达了结束节点,引擎会自动触发“流程结束结算”流程:清理运行时数据,归档历史数据。
3. 数据库操作 (级联发生)
这一步的操作非常剧烈,涉及多张表的联动:
-
A. 运行时数据清理 (Runtime - RU)
-
ACT_RU_EXECUTION: DELETE。- 因为跳转到了
EndEvent,流程实例判定结束,引擎删除该实例的所有执行流记录。
- 因为跳转到了
-
ACT_RU_TASK: DELETE。- 当前活着的任务被物理删除。
-
ACT_RU_VARIABLE: DELETE。- 第一步和第二步插入的所有变量,在这里会被物理删除(但别怕,看下面)。
-
B. 历史数据归档 (History - HI)
-
ACT_HI_PROCINST: UPDATE。- 更新
END_TIME_(结束时间),DELETE_REASON_(通常记录为 ChangeActivityState)。
- 更新
-
ACT_HI_TASKINST: UPDATE。- 找到刚才删除的任务,更新
END_TIME_,标记为已完结/已删除。
- 找到刚才删除的任务,更新
-
ACT_HI_VARINST(历史变量表): INSERT。- 这是最重要的一点! 引擎在删除
ACT_RU_VARIABLE之前,会把里面所有数据(包括我们在第一步、第二步存的 CANCEL 状态和原因)搬运到这张历史表中。
- 这是最重要的一点! 引擎在删除
为什么物理终止流程changeState()前,要先在VARIABLE表中插入流程实例和任务的STATUS和REASON
查询“我的流程”状态时,会同步查询 历史流程表(确定流程是否结束)和 历史变量表(确定业务状态 STATUS 和 REASON)。
如果没有往 VARIABLE 中插入记录就直接终止流程:
- 用户会看到对应的流程实例:显示为 “正常完成”(或“审批通过”)。
- 因为引擎只知道流程“走到了终点”,不知道是被“强制取消”的,导致严重的业务误判。
- 对应的任务会:显示为 “不明原因结束”(或“已完成”)。
- 历史记录里查不到该任务的取消标记,用户无法区分这是自己审批过的,还是被系统强制掐断的。
什么时候,记录会被放到ACT_HI_表中
很多人误以为“历史表”是等流程结束后才把数据“搬”过去的。
大错特错!
真相是: ACT_HI_... 表的数据是伴随着运行表 ACT_RU_... 同步生成的,就像写日记一样,发生一件记一件,而不是等过完一辈子才一次性写回忆录。
只有一种情况是真正的“搬家”(Runtime 删了,只留 History),那就是任务完成或流程结束的那一瞬间。
我们用一个**“张三请假”**的例子,分三个阶段带你把数据库看透
场景设定
- 流程:请假流程 (
leave_process) - 操作人:张三(申请人)、李四(审批人)
- 历史级别:
Audit或Full(默认配置,若设为 None 则不记录历史)
第一阶段:流程刚发起(Start)
动作:张三点击“提交申请”,流程启动,并自动进入第一个节点“经理审批”。
此时,数据是同时写入 Runtime 和 History 表的:
-
流程实例表:
ACT_RU_EXECUTION: INSERT 一条记录(我活着)。ACT_HI_PROCINST: INSERT 一条记录(我出生了)。- 状态:
START_TIME_有值,END_TIME_为 NULL。
-
任务表(经理审批):
ACT_RU_TASK: INSERT 一条记录(李四有个待办)。ACT_HI_TASKINST: INSERT 一条记录(历史记录里记一笔:李四产生了一个任务)。- 状态:
START_TIME_有值,END_TIME_为 NULL。
-
变量表(请假原因):
ACT_RU_VARIABLE: INSERTreason = "生病".ACT_HI_VARINST: INSERTreason = "生病".
结论:在这个阶段,你查 Runtime 表和 History 表,都能查到数据,而且是一模一样的。
第二阶段:修改变量(Update)
动作:在审批前,系统自动运行了一个 Listener,把请假天数从 3 天改成了 5 天。
- 变量表:
ACT_RU_VARIABLE: UPDATEdays从 3 变 5。ACT_HI_VARINST: UPDATEdays从 3 变 5(如果是Full级别,会有ACT_HI_DETAIL记录修改明细)。
结论:Runtime 表变了,History 表立刻同步更新。History 表并不是死数据,它是活的审计日志。
第三阶段:任务完成/流程结束(Complete/End)—— 关键分水岭
动作:李四点击“同意”,流程走到 EndEvent 结束。
这一刻,发生了真正的“断舍离”:
-
任务表:
ACT_RU_TASK: DELETE! (李四的待办列表清空了)。ACT_HI_TASKINST: UPDATE!- 把这条记录的
END_TIME_填上当前时间。 - 把
DELETE_REASON_填上completed。
-
流程实例表:
ACT_RU_EXECUTION: DELETE! (内存释放,引擎不再维护这个实例)。ACT_HI_PROCINST: UPDATE!- 把
END_TIME_填上当前时间。
-
变量表:
ACT_RU_VARIABLE: DELETE! (运行时不需要这些变量了)。ACT_HI_VARINST: 不变 (或者更新最后一次的值)。数据永久保留。
审批不通过,后端经过了什么操作,数据库层面有什么操作
针对默认的“审批不通过(拒绝)并结束流程”逻辑,核心流程分为三步:任务打标 -> 实例打标 -> 物理终止。
1. 核心代码摘要
// 1. 【任务层】标记当前任务为 REJECT (不通过)
updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), reqVO.getReason());
// 2. 【实例层】标记整个流程为 REJECT (不通过)
processInstanceService.updateProcessInstanceReject(instance, reqVO.getReason());
// 3. 【物理层】强制跳转到结束节点 (物理终止)
moveTaskToEnd(task.getProcessInstanceId(), ...);
2. 数据库操作详解
第一步:任务打标 (Task Level)
给当前这个任务贴上“不通过”的标签,以便历史记录查询。
- 代码:
updateTaskStatusAndReason(task.getId(), ...) - 操作表:
ACT_RU_VARIABLE(运行时变量表) - 操作动作:INSERT
- 关键数据:
TASK_ID_: 当前任务ID (绑定在任务上)NAME_:bpm_task_statusTEXT_:REJECT
第二步:实例打标 (Instance Level)
给整个流程实例贴上“不通过”的标签,防止前端显示为“审批通过”。
- 代码:
updateProcessInstanceReject(instance, ...) - 操作表:
ACT_RU_VARIABLE(运行时变量表) - 操作动作:INSERT
- 关键数据:
PROC_INST_ID_: 当前流程ID (绑定在流程上,TASK_ID_为空)NAME_:bpm_process_instance_statusTEXT_:REJECT
第三步:物理终止 (Physical Termination)
利用 Flowable 引擎能力,强制结束流程。
- 代码:
moveTaskToEnd(...)->changeState(...) - 操作表:
-
ACT_RU_EXECUTION(运行时执行表): DELETE。- 指针跳转到 EndEvent,流程判定结束,删除执行流。
-
ACT_RU_TASK(运行时任务表): DELETE。- 当前任务被物理删除。
-
ACT_HI_VARINST(历史变量表): INSERT。- 关键点:引擎在删除 Runtime 变量前,会自动把前两步写入的
REJECT状态搬运到这张历史表。
- 关键点:引擎在删除 Runtime 变量前,会自动把前两步写入的
-
ACT_HI_PROCINST(历史流程表): UPDATE。- 更新
END_TIME_,标记流程已结束。
- 更新
-
更多推荐

所有评论(0)