Activiti 从零到生产级掌握
工作流最容易学成“画流程图 + 调几个 API”。但商业系统里真正难的是:流程图如何驱动业务状态,待办和已办怎么查,审批权限怎么控制,驳回撤回怎么设计,流程版本怎么兼容,流程表和业务表怎么关联,线上任务卡住怎么排查。
一句话建立主线:
Activiti 是 BPMN 工作流引擎,它把流程定义部署成可执行模型,再在运行时创建流程实例、生成任务、判断网关、保存变量和历史记录;业务系统负责业务数据、权限、表单、通知和最终业务状态。
学习目标
学完这一页,你要能做到:
- 解释 BPMN、流程定义、流程实例、任务、变量、网关、历史表分别是什么。
- 解释 Activiti 从部署流程到完成审批的完整执行链路。
- 设计业务表和流程实例的关联方式。
- 设计待办、已办、我的申请、流程轨迹查询。
- 解释用户任务、候选人、候选组、认领、完成任务的区别。
- 解释排他网关、并行网关、包容网关的使用边界。
- 设计通过、驳回、撤回、转办、委派、加签、会签。
- 解释流程版本升级后旧实例和新实例如何共存。
- 能写启动流程、完成任务、查询待办、查询轨迹的 Demo。
- 能排查任务不出现、流程卡住、条件不走、变量丢失、历史查不到。
如果要按“零基础能看懂、原理能讲清、项目能落地、面试能回答、线上能排查”的标准验收,请配合阅读 Activiti 从零到精通验收清单。本页负责建立主线,验收清单负责把 BPMN、引擎表、任务流转、网关、驳回撤回、版本、一致性和排查逐项拆开。
工作流适合什么场景
适合 Activiti 的场景通常有这些特征:
| 特征 | 说明 | 例子 |
|---|---|---|
| 多节点 | 不止一步处理 | 申请 -> 部门审批 -> 财务审批 |
| 多角色 | 不同角色处理不同节点 | 员工、主管、财务、管理员 |
| 条件分支 | 根据金额、类型、区域走不同流程 | 金额大于 1 万走总监 |
| 人工待办 | 需要人登录系统处理 | 待审批、待复核、待归档 |
| 留痕审计 | 要知道谁在什么时候做了什么 | 医疗数据审批、合同审核 |
| 版本变化 | 流程规则会变化 | 新旧审批规则并存 |
不适合的场景:
- 只有简单状态流转,例如订单待支付、已支付、已取消,状态机更轻。
- 高频毫秒级链路,工作流引擎和历史表会带来额外开销。
- 纯技术编排,例如微服务调用编排,通常用 Saga、任务编排或消息驱动更合适。
核心对象关系
flowchart TD
A["BPMN 文件"] --> B["流程定义 ProcessDefinition"]
B --> C["流程实例 ProcessInstance"]
C --> D["执行实例 Execution"]
C --> E["用户任务 Task"]
C --> F["流程变量 Variable"]
E --> G["历史任务 HistoricTaskInstance"]
C --> H["历史流程 HistoricProcessInstance"]
I["业务表 business_id"] --> C要特别理解业务表和流程表的边界:
| 数据 | 应该放哪里 | 原因 |
|---|---|---|
| 请假天数、申请原因、合同金额 | 业务表 | 业务查询、报表、权限都依赖它 |
| 当前审批节点 | Activiti 运行时表,也可冗余到业务表 | 引擎负责准确流转,业务表方便列表展示 |
| 申请状态 | 业务表 | 页面和业务逻辑最终看业务状态 |
| 审批意见 | 可以业务表记录,也可结合历史任务 | 需要长期审计和业务展示 |
| 流程变量 | Activiti 表 | 网关判断和流程上下文 |
不要把大 JSON、附件、完整业务对象都塞进流程变量。流程变量适合保存流程判断需要的小字段,例如金额、部门、审批结果、申请人 ID。
完整执行链路
flowchart TD
A["设计 BPMN"] --> B["部署流程定义"]
B --> C["创建业务单据"]
C --> D["启动流程实例"]
D --> E["保存 businessKey 和 processInstanceId"]
E --> F["生成第一个用户任务"]
F --> G["查询待办"]
G --> H["用户审批"]
H --> I["设置变量并完成任务"]
I --> J["引擎判断网关和连线条件"]
J --> K{"是否还有节点"}
K -- "有" --> F
K -- "无" --> L["流程结束"]
L --> M["更新业务状态和保存历史"]这条链路里最容易出问题的是两处:
- 启动流程和更新业务表必须在同一个事务里,否则会出现业务单据已提交但流程没启动,或流程启动了但业务单据状态没变。
- 完成任务和更新业务状态也要保持一致,否则会出现流程已到下一节点,但业务列表仍显示旧状态。
表结构怎么理解
Activiti 常见表可以按前缀理解:
| 前缀 | 含义 | 常见用途 |
|---|---|---|
ACT_RE_* | Repository,流程定义资源 | 流程部署、BPMN 文件、定义版本 |
ACT_RU_* | Runtime,运行时数据 | 正在运行的流程、任务、变量 |
ACT_HI_* | History,历史数据 | 已办、轨迹、审计 |
ACT_ID_* | Identity,用户组织 | 用户、组,很多项目不用它 |
ACT_GE_* | General,通用数据 | 二进制资源、属性 |
运行时表和历史表的区别:
flowchart TD
A["流程运行中"] --> B["ACT_RU_TASK"]
A --> C["ACT_RU_EXECUTION"]
A --> D["ACT_RU_VARIABLE"]
E["流程节点完成"] --> F["写入 ACT_HI_TASKINST"]
E --> G["写入 ACT_HI_ACTINST"]
H["流程结束"] --> I["运行时数据清理"]
I --> J["历史数据保留"]所以已办查询通常不是查运行时任务表,而是查历史任务表。
最小业务表设计
CREATE TABLE leave_apply (
id BIGINT PRIMARY KEY,
apply_user_id BIGINT NOT NULL,
days INT NOT NULL,
reason VARCHAR(500),
status VARCHAR(32) NOT NULL,
process_instance_id VARCHAR(64),
current_node_name VARCHAR(100),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE leave_approve_record (
id BIGINT PRIMARY KEY,
apply_id BIGINT NOT NULL,
task_id VARCHAR(64) NOT NULL,
approver_id BIGINT NOT NULL,
action VARCHAR(32) NOT NULL,
comment VARCHAR(500),
created_at DATETIME NOT NULL
);为什么要有业务审批记录表:
- Activiti 历史表偏流程引擎视角,业务页面展示不一定方便。
- 审批意见、附件、签名、业务备注可能需要自定义字段。
- 后续换流程引擎时,业务审计数据不能完全绑死在引擎表。
启动流程 Demo
@Service
public class LeaveWorkflowService {
private final RuntimeService runtimeService;
private final LeaveApplyMapper leaveApplyMapper;
@Transactional
public String start(Long applyId, Long applyUserId, int days) {
LeaveApply apply = leaveApplyMapper.selectById(applyId);
if (apply == null) {
throw new IllegalArgumentException("申请单不存在");
}
if (!"DRAFT".equals(apply.getStatus())) {
throw new IllegalStateException("只有草稿状态可以发起");
}
Map<String, Object> variables = new HashMap<>();
variables.put("applyUserId", applyUserId);
variables.put("days", days);
variables.put("approved", false);
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"leave_process",
applyId.toString(),
variables
);
apply.setStatus("APPROVING");
apply.setProcessInstanceId(instance.getProcessInstanceId());
apply.setCurrentNodeName("提交申请");
leaveApplyMapper.updateById(apply);
return instance.getProcessInstanceId();
}
}关键点:
processDefinitionKey是 BPMN 中的流程 key。businessKey用业务 ID,方便从流程反查业务单。- 启动流程和更新业务表放在同一个事务。
- 业务状态不要只依赖 Activiti 表,业务系统要有自己的状态字段。
查询待办 Demo
public List<TaskDTO> todoList(Long userId, List<String> roleCodes) {
List<Task> personalTasks = taskService.createTaskQuery()
.taskAssignee(userId.toString())
.active()
.list();
List<Task> candidateTasks = taskService.createTaskQuery()
.taskCandidateGroupIn(roleCodes)
.active()
.list();
return Stream.concat(personalTasks.stream(), candidateTasks.stream())
.map(task -> new TaskDTO(
task.getId(),
task.getName(),
task.getProcessInstanceId(),
task.getCreateTime()
))
.collect(Collectors.toList());
}候选组任务和个人任务的区别:
| 类型 | 含义 | 常见操作 |
|---|---|---|
| assignee | 已指定处理人 | 直接完成 |
| candidate user | 候选用户 | 先 claim 再完成 |
| candidate group | 候选角色/组 | 组内用户可认领 |
如果候选组任务不认领就直接完成,权限边界会混乱;如果认领后不释放,其他人可能看不到任务。
完成审批 Demo
@Transactional
public void approve(String taskId, Long approverId, boolean approved, String comment) {
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
if (task == null) {
throw new IllegalArgumentException("任务不存在或已处理");
}
String processInstanceId = task.getProcessInstanceId();
ProcessInstance instance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
Long applyId = Long.valueOf(instance.getBusinessKey());
checkApprovePermission(task, approverId);
Map<String, Object> variables = new HashMap<>();
variables.put("approved", approved);
variables.put("approverId", approverId);
taskService.addComment(taskId, processInstanceId, comment);
taskService.complete(taskId, variables);
leaveApproveRecordMapper.insert(buildRecord(applyId, taskId, approverId, approved, comment));
if (!approved) {
leaveApplyMapper.updateStatus(applyId, "REJECTED");
} else if (isProcessEnded(processInstanceId)) {
leaveApplyMapper.updateStatus(applyId, "APPROVED");
} else {
leaveApplyMapper.updateCurrentNode(applyId, findCurrentNodeName(processInstanceId));
}
}这里有一个非常重要的原则:完成任务前必须校验权限。不能只因为前端按钮隐藏了,就认为用户不能提交请求。
网关怎么选
| 网关 | 原理 | 适合场景 | 用错后果 |
|---|---|---|---|
| 排他网关 | 多条路只走一条 | 金额不同走不同审批人 | 条件重叠时结果不符合预期 |
| 并行网关 | 多条路都走,等全部汇聚 | 多部门同时会签 | 某一路没完成会一直卡住 |
| 包容网关 | 满足条件的多条路都走 | 可选多部门审批 | 条件设计复杂,排查成本高 |
排他网关示例:
flowchart TD
A["主管审批"] --> B{"请假天数"}
B -- "days <= 3" --> C["流程结束"]
B -- "days > 3" --> D["经理审批"]
D --> E["流程结束"]变量名写错、变量类型不对、条件表达式不互斥,都会导致流程不按预期走。
驳回、撤回、转办、加签怎么设计
| 能力 | 含义 | 推荐做法 |
|---|---|---|
| 驳回 | 审批人拒绝申请 | 简单场景走拒绝分支;复杂场景用业务动作和流程跳转 |
| 撤回 | 发起人撤回未完成流程 | 校验当前节点允许撤回,再删除或终止实例并更新业务状态 |
| 转办 | 当前处理人把任务转给别人 | 修改 assignee 并记录操作 |
| 委派 | 让别人协助处理,完成后回到委派人 | 使用委派语义或业务自定义 |
| 加签 | 临时增加审批人 | 简单系统用业务表加审批记录,复杂系统动态加节点需谨慎 |
| 会签 | 多人共同审批 | 使用多实例任务,明确通过比例和否决规则 |
不要为了“看起来灵活”把任意跳转开放给前端。任意跳转很容易破坏流程语义,导致历史轨迹和业务状态对不上。
流程版本
流程定义每次部署会形成新版本。老流程实例默认继续按老版本走,新发起实例可以使用新版本。
flowchart TD
A["leave_process v1"] --> B["旧流程实例 1001"]
C["重新部署 BPMN"] --> D["leave_process v2"]
D --> E["新流程实例 2001"]
B --> F["继续按 v1 执行"]
E --> G["按 v2 执行"]生产注意:
- 不要直接删除旧流程定义,旧实例可能还依赖它。
- BPMN key 要稳定,否则新流程不是升级,而是另一个流程。
- 表单字段变化要兼容旧实例。
- 流程升级前要确认是否需要迁移未完成实例。
商业场景:医疗数据资产审批
场景:医院数据资产平台中,科室提交数据集发布申请,需要数据管理员审核、信息科安全审核、负责人终审,最终发布到目录。
flowchart TD
A["科室提交数据集"] --> B["数据管理员审核"]
B --> C{"数据质量是否通过"}
C -- "否" --> D["驳回科室修改"]
C -- "是" --> E["信息科安全审核"]
E --> F{"是否包含敏感字段"}
F -- "是" --> G["脱敏方案确认"]
F -- "否" --> H["负责人终审"]
G --> H
H --> I{"是否通过"}
I -- "通过" --> J["发布数据资产目录"]
I -- "拒绝" --> D设计要点:
- 业务表保存数据集 ID、申请人、状态、当前节点。
- 流程变量只保存是否敏感、数据等级、审核结果等判断字段。
- 每个审批动作写业务审批记录。
- 发布目录是业务动作,不能只依赖流程结束事件。
- 审批权限按科室、角色、数据等级校验。
线上排查
flowchart TD
A["工作流问题"] --> B{"表现"}
B -- "没有待办" --> C["查 assignee、candidate、流程是否到用户任务"]
B -- "流程卡住" --> D["查当前 execution、网关条件、并行分支"]
B -- "变量不对" --> E["查启动和完成任务时变量名与类型"]
B -- "历史查不到" --> F["查 history level 和历史表"]
B -- "业务状态错" --> G["查业务事务和流程事务是否一致"]
C --> H["TaskQuery 和 ACT_RU_TASK"]
D --> I["ACT_RU_EXECUTION 和流程图"]
E --> J["ACT_RU_VARIABLE 或 ACT_HI_VARINST"]常用排查清单:
| 现象 | 可能原因 | 证据 |
|---|---|---|
| 待办列表为空 | assignee 不一致、候选组没传、任务已被认领 | ACT_RU_TASK |
| 完成任务报错 | taskId 错、任务已完成、无权限 | 任务查询和业务日志 |
| 网关不走 | 变量名错、类型错、表达式不匹配 | 流程变量表 |
| 流程结束但业务未更新 | 事务边界不一致、监听器失败 | 业务表和流程历史 |
| 历史记录少 | history level 配置低 | Activiti 配置 |
| 新流程没生效 | 启动用错 key 或旧实例仍走旧版本 | 流程定义版本 |
面试标准回答
Activiti 是什么
Activiti 是基于 BPMN 的工作流引擎。它把流程图部署成流程定义,运行时根据定义创建流程实例、生成用户任务、判断网关条件、保存变量和历史记录。业务系统负责业务表、权限、表单、通知和业务状态,不能把所有业务逻辑都塞进流程引擎。业务表和流程表怎么关联
通常业务表保存业务主键、业务状态、当前节点和 processInstanceId;启动流程时把业务主键作为 businessKey 传给流程实例。这样可以从业务单据查流程,也可以从流程实例反查业务。流程变量只保存网关判断需要的小字段,大业务对象仍放业务表。待办和已办怎么查
待办查运行时任务,通常通过 assignee、candidate user 或 candidate group 过滤;已办查历史任务表,根据处理人、流程实例、业务 key 等条件查询。待办是正在等待处理的任务,已办是已经处理过的历史记录,不能混用同一张表理解。流程卡住怎么排查
先看当前流程实例是否还存在,再查 ACT_RU_TASK 是否有用户任务。如果没有任务,要看 execution 卡在哪个活动,重点排查网关条件、并行分支是否全部到达、变量名和类型是否正确。如果业务状态和流程状态不一致,还要查启动流程、完成任务和更新业务表是否在同一事务边界内。关联知识点
| 知识点 | 继续学习 |
|---|---|
| 从零到精通验收 | Activiti 从零到精通验收清单 |
| 商业场景训练营 | Activiti 商业场景训练营 |
| Activiti 总览 | 从零学习路线、引擎架构与首个流程 |
| 流程设计 | BPMN可执行模型、事件、任务、子流程与会签 |
| 网关 | Execution、排他、并行、包容与汇聚原理 |
| 任务流转 API | 任务生命周期、认领、委派、并发与待办已办 |
| 业务一致性 | 本地事务、跨库命令、Outbox、补偿与对账 |
| 部署与版本 | 部署落表、版本、缓存、灰度与回退 |
| Spring 事务 | Spring 事务 |
| 分布式事务 | 分布式事务 |
本章小结
Activiti 学习的关键不是会画 BPMN,而是把“流程定义、流程实例、任务、变量、网关、历史、业务表、权限、事务、版本、排查”串起来。商业项目里流程引擎只负责流程运行,真正的业务正确性仍然依赖业务表设计、权限校验、事务边界和可观测性。
