Activiti从零学习路线与工作原理总览
Activiti 是 BPMN 工作流引擎。它把 BPMN XML 解析为可执行流程定义,在运行时创建流程实例、Execution、User Task、Variable、Timer/Async Job,并把执行轨迹写入历史表。
它不是一个完整审批产品,也不是业务数据库。表单、领域状态、登录认证、数据权限、组织架构、通知、附件、外部系统一致性和最终商业结果仍要由业务系统设计。
本页是 Activiti 专栏入口。先建立完整心智模型并跑通一个最小流程,再按“BPMN 建模 → 任务 → 网关 → 一致性 → 部署版本 → 生产排查 → 面试”的顺序深入。
Activiti 不同大版本的 Starter、包名、配置属性、动态迁移和云组件差异较大。项目应先锁定正在使用的准确版本,再对照该版本 API 和源码;本专栏重点讲跨版本稳定的执行原理,并在实现差异处明确说明。
学习目标
完成本页后,你应该能够:
- 判断业务应该使用工作流、状态机、任务调度还是普通代码。
- 区分流程定义、Deployment、流程实例、Execution、Task、Variable 和 History。
- 解释 ProcessEngine、RepositoryService、RuntimeService、TaskService、HistoryService 和 Job Executor 的职责。
- 解释 BPMN XML 从部署到创建流程实例、用户任务和历史的完整过程。
- 写出一个最小 BPMN,并用 Java 部署、启动、查询和完成任务。
- 区分 Runtime 表与 History 表,知道待办和已办为什么不能查同一张表。
- 理解业务表与流程表为什么必须分离又必须关联。
- 知道版本、权限、事务、幂等、异步 Job 和对账为什么是生产必需能力。
- 按本专栏路线继续学习每个深层知识点。
一、Activiti到底解决什么问题
假设医疗数据资产发布要经过:申请人提交、部门负责人审批;涉及敏感字段时增加安全审核;跨境数据增加合规审核;全部通过后发布目录;任何拒绝都要留痕。
如果全部写在业务代码中,会逐渐出现:
Controller判断角色
Service写大量if/else
数据库只存一个status
定时任务扫描超时审批
消息消费者推进下一状态
前端自己判断按钮
历史记录散落在日志和业务表Activiti 将“过程如何推进”显式化:
flowchart TD
A["提交数据资产"] --> B["负责人审批"]
B --> C{"是否包含高风险数据"}
C -->|"否"| D["发布目录"]
C -->|"是"| E["安全与合规审核"]
E --> D
B -->|"拒绝"| F["记录拒绝并结束"]引擎负责:
- 保存流程定义和版本。
- 创建一次具体流程实例。
- 生成谁需要办理的 User Task。
- 根据变量和网关推进 Execution。
- 持久化等待状态,不占用 Web 线程持续阻塞。
- 调度 Timer/Async Job。
- 保存可查询的流程历史。
业务系统负责:
- 资产、合同、报销单等领域数据。
- 申请、审批和发布业务状态。
- 登录认证、组织和数据权限。
- 审批意见和长期审计。
- 外部接口、MQ、Outbox、幂等和补偿。
- 页面表单、附件、通知和报表。
二、哪些场景适合工作流
适合:
- 多个人或角色参与。
- 流程跨越分钟、小时、天甚至月。
- 有待办、已办、转办、委派、超时和留痕。
- 分支、并行、会签和事件等待比较明显。
- 流程定义需要版本化并与业务沟通。
商业场景:
- 合同审批。
- 采购和报销审批。
- 医疗数据资产发布和敏感数据授权。
- 客服工单与 SLA 升级。
- 账户开通、多部门审核。
- 内容发布、法务和合规审查。
三、哪些场景不应该上Activiti
| 场景 | 更适合 | 原因 |
|---|---|---|
| 订单待支付→已支付→已取消 | 领域状态机 | 状态少、转换短、吞吐高,不一定需要人工待办 |
| 每天凌晨同步报表 | 调度平台/XXL-JOB | 核心是定时和分片,不是人工流程 |
| 服务A调用B再调用C | 应用编排/代码/消息 | 短事务调用链不必引入人工工作流 |
| 简单一次性校验 | 普通Service代码 | BPMN和引擎成本高于收益 |
| 大规模实时流处理 | Kafka Streams/Flink等 | 工作流引擎不是高吞吐流计算平台 |
不要因为“以后可能改流程”就把所有状态变更放进 Activiti。工作流有模型、表、版本、Job、历史和运维成本。
四、工作流、状态机、任务调度和Saga区别
| 能力 | 工作流 | 状态机 | 任务调度 | Saga/分布式事务协调 |
|---|---|---|---|---|
| 核心对象 | 流程定义、实例、Task、Event | State、Event、Transition | Job、Trigger、Executor | 本地事务步骤和补偿 |
| 人工任务 | 强 | 通常需自己实现 | 弱 | 不是重点 |
| 长时间等待 | 原生持久化 | 可实现 | 适合定时触发 | 可跨长流程但关注一致性 |
| BPMN可视化 | 是 | 通常不是BPMN | 否 | 可用编排模型但语义不同 |
| 历史审计 | 引擎提供流程视角 | 自己设计 | 调度执行历史 | 事务/补偿历史 |
| 典型场景 | 审批、工单 | 订单状态 | 定时批处理 | 跨服务业务事务 |
一个系统可以组合使用:订单状态由状态机维护,退款审批由工作流协调,夜间对账由调度平台执行,跨服务退款使用 Saga/消息补偿。
五、Activiti核心对象关系
flowchart TD
A["BPMN XML资源"] --> B["Deployment:一次部署批次"]
B --> C["ProcessDefinition V1"]
B --> D["ProcessDefinition V2或其他process"]
C --> E["ProcessInstance:业务单A的一次运行"]
C --> F["ProcessInstance:业务单B的一次运行"]
E --> G["Execution:当前执行路径"]
G --> H["Task:等待人工处理"]
G --> I["Variable:流程上下文"]
G --> J["Job/Event Subscription:异步和等待"]
E --> K["History:实例、Activity、Task轨迹"]| 对象 | 类比 | 准确含义 |
|---|---|---|
| BPMN Resource | 源代码 | 可执行流程XML和图形资源 |
| Deployment | 一次发布包 | 一次资源部署批次,可含多个定义 |
| ProcessDefinition | 类/模板版本 | 某个process key的具体版本 |
| ProcessInstance | 对象实例 | 某张业务单的一次流程运行 |
| Execution | 执行Token/路径 | 引擎当前走到哪里,并行时可有多个 |
| Task | 人工工作项 | User Task产生的待办,不等于业务单 |
| Variable | 执行上下文 | 网关/办理人等必要小数据,不是业务数据库 |
| History | 执行轨迹 | 已发生实例、节点和任务记录 |
六、ProcessEngine内部有哪些层
flowchart TD
A["业务应用"] --> B["Repository/Runtime/Task/History等Service门面"]
B --> C["Command Executor和Command Context"]
C --> D["Agenda/Atomic Operation推进Execution"]
C --> E["Entity Manager和Session"]
E --> F["数据库Runtime/History/Repository表"]
G["Job Executor"] --> C
H["BPMN Parser与Deployment Cache"] --> C内部类名会随 Activiti 大版本变化,但稳定思路是:公开 Service 不是简单 DAO,而是把操作封装为引擎 Command,在统一上下文和事务中维护多个相关实体。
6.1 Service门面
业务代码通过公开 API 使用引擎,不应直接修改 ACT 表。
6.2 Command Context
统一管理一次引擎命令中的持久化、缓存、Agenda 和事务资源。taskService.complete 会推进后续节点,不只是删除 Task。
6.3 Deployment Cache
缓存已经解析的流程模型,避免每次推进都重新读取和解析 BPMN。缓存丢失可从数据库资源重建。
6.4 Job Executor
扫描可执行 Timer/Async Job,抢占、执行、失败重试。它在独立线程和事务中运行,不继承发起 Web 请求的本地事务。
七、六个最常用Service
| Service | 主要职责 | 常见API |
|---|---|---|
RepositoryService | 部署、查询、挂起流程定义 | createDeployment、createProcessDefinitionQuery |
RuntimeService | 启动和查询运行实例、变量、事件 | startProcessInstanceByKey、createProcessInstanceQuery |
TaskService | 待办、认领、完成、委派、评论 | createTaskQuery、claim、complete |
HistoryService | 已办、历史实例、Activity和变量 | createHistoricTaskInstanceQuery |
ManagementService | Job、数据库表和管理能力 | 具体API按版本 |
IdentityService | 引擎用户组和authenticated user | 企业系统常需对接外部IAM |
Service 职责可以重叠关联,但不要用 RepositoryService 启动实例,也不要用 Runtime Task 查询已办。
八、一次流程从部署到完成的全过程
flowchart TD
A["RepositoryService部署BPMN"] --> B["资源和ProcessDefinition写入数据库"]
B --> C["业务提交申请"]
C --> D["RuntimeService按key/definitionId启动实例"]
D --> E["创建根Execution并进入Start Event"]
E --> F["沿Sequence Flow到User Task"]
F --> G["创建Task和候选Identity Link"]
G --> H["用户查询/认领待办"]
H --> I["业务服务校验权限并complete"]
I --> J["Execution继续并计算Gateway"]
J --> K{"是否还有等待节点"}
K -->|"有"| L["创建Task、Job或事件订阅"]
L --> H
K -->|"无且到达End"| M["结束运行实例"]
M --> N["Runtime数据清理,History保留轨迹"]每一步都可能失败,排查要锁定 businessKey → processInstanceId → processDefinitionId → current Activity/Task/Job,不能只看业务状态。
九、Runtime表和History表
常见前缀:
| 前缀 | 含义 | 示例 |
|---|---|---|
ACT_RE_* | Repository,部署和定义 | ACT_RE_DEPLOYMENT、ACT_RE_PROCDEF |
ACT_RU_* | Runtime,当前运行状态 | ACT_RU_EXECUTION、ACT_RU_TASK |
ACT_HI_* | History,已经发生的轨迹 | ACT_HI_PROCINST、ACT_HI_TASKINST |
ACT_GE_* | General,通用资源/属性 | ACT_GE_BYTEARRAY |
ACT_ID_* | Identity,用户和组 | 是否使用取决于身份集成 |
版本不同还会有 Job、Event、Form 等其他表。不能仅凭表名前缀直接更新。
为什么Task完成后ACT_RU_TASK查不到
Runtime 表只保存当前活动待办。Task 完成后应从 Runtime 移除,并根据 History Level 写历史。待办查 Runtime,已办查 History 加业务审批记录。
流程结束后Runtime实例为什么消失
流程结束意味着不再需要维持活动 Execution;历史实例保留起止和轨迹。业务表仍需保存最终领域状态。
十、首个可执行BPMN
保存为 src/main/resources/processes/leave_approval.bpmn20.xml:
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:activiti="http://activiti.org/bpmn"
targetNamespace="https://example.com/processes">
<process id="leave_approval"
name="请假审批"
isExecutable="true">
<startEvent id="start" name="提交申请"/>
<userTask id="managerReview"
name="主管审批"
activiti:candidateGroups="LEAVE_MANAGER"/>
<exclusiveGateway id="decisionGateway"
name="审批结果"
default="toRejected"/>
<endEvent id="approvedEnd" name="审批通过"/>
<endEvent id="rejectedEnd" name="审批拒绝"/>
<sequenceFlow id="f1" sourceRef="start" targetRef="managerReview"/>
<sequenceFlow id="f2" sourceRef="managerReview" targetRef="decisionGateway"/>
<sequenceFlow id="toApproved"
sourceRef="decisionGateway"
targetRef="approvedEnd">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[
${approved == true}
]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="toRejected"
sourceRef="decisionGateway"
targetRef="rejectedEnd"/>
</process>
</definitions>这里故意将拒绝作为 Default Flow:变量缺失或不为 Boolean true 时不会默认通过。实际业务应区分“明确拒绝”和“变量异常人工处理”,不能把所有异常静默当拒绝。
十一、部署Demo
@Service
public class LeaveProcessDeploymentService {
private final RepositoryService repositoryService;
public LeaveProcessDeploymentService(
RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}
@Transactional
public ProcessDefinition deploy() {
Deployment deployment = repositoryService.createDeployment()
.name("请假审批流程")
.addClasspathResource(
"processes/leave_approval.bpmn20.xml")
.deploy();
ProcessDefinition definition = repositoryService
.createProcessDefinitionQuery()
.deploymentId(deployment.getId())
.processDefinitionKey("leave_approval")
.singleResult();
if (definition == null) {
throw new IllegalStateException("部署后未找到流程定义");
}
return definition;
}
}部署成功只证明资源持久化和模型解析成功,不证明候选组、变量、Listener 和每条运行分支都正确。
十二、启动流程Demo
@Service
public class LeaveApprovalService {
private final RuntimeService runtimeService;
private final LeaveApplicationRepository applicationRepository;
public LeaveApprovalService(
RuntimeService runtimeService,
LeaveApplicationRepository applicationRepository) {
this.runtimeService = runtimeService;
this.applicationRepository = applicationRepository;
}
@Transactional
public String submit(Long applicationId, String operatorId) {
LeaveApplication application = applicationRepository
.findByIdForUpdate(applicationId)
.orElseThrow(() -> new IllegalArgumentException("申请不存在"));
if (!application.canSubmit(operatorId)) {
throw new IllegalStateException("状态或权限不允许提交");
}
Map<String, Object> variables = new HashMap<>();
variables.put("applicantId", operatorId);
variables.put("leaveDays", application.getLeaveDays());
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"leave_approval",
applicationId.toString(),
variables);
application.markApproving(
instance.getProcessInstanceId(),
instance.getProcessDefinitionId());
return instance.getProcessInstanceId();
}
}前提是业务表与 Activiti 表真正使用同一事务资源;跨库时不能依赖普通 @Transactional,详见一致性专栏。
十三、查询与完成Task Demo
public List<Task> listCandidateTasks(String userId) {
return taskService.createTaskQuery()
.taskCandidateUser(userId)
.active()
.orderByTaskCreateTime()
.desc()
.listPage(0, 20);
}@Transactional
public void decide(
String taskId,
String operatorId,
String requestId,
boolean approved) {
Task task = taskService.createTaskQuery()
.taskId(taskId)
.active()
.singleResult();
if (task == null) {
// 应结合requestId和审批记录区分幂等重试与错误taskId
throw new IllegalStateException("任务不存在或已处理");
}
permissionService.assertCanComplete(operatorId, task);
approvalRecordService.assertRequestNotProcessed(requestId);
Map<String, Object> variables = new HashMap<>();
variables.put("approved", approved);
variables.put("lastApproverId", operatorId);
taskService.complete(taskId, variables);
approvalRecordService.record(
requestId,
task,
operatorId,
approved);
}真正生产实现要使用数据库唯一约束防并发重复,不能只用 assertRequestNotProcessed 的先查询逻辑。完整实现见任务专栏。
十四、业务表和流程表怎样关联
flowchart TD
A["业务主键applicationId"] --> B["作为businessKey启动实例"]
B --> C["Activiti生成processInstanceId"]
C --> D["业务表保存processInstanceId和definitionId"]
D --> E["业务查流程:使用processInstanceId"]
C --> F["流程查业务:使用businessKey"]业务表保存:领域字段、业务状态、流程实例关系、审批记录和版本快照。流程变量只保存网关、办理人和执行上下文需要的小字段。
深入见:业务表、流程表与分布式一致性。
十五、权限为什么不能交给前端或Activiti单独完成
完成审批至少校验:
flowchart TD
A["用户登录身份"] --> B["API权限"]
B --> C["Task assignee/candidate/owner关系"]
C --> D["业务单数据权限"]
D --> E["当前业务状态和Task节点"]
E --> F["动作权限:通过/拒绝/转办等"]
F --> G["并发和幂等校验"]Activiti 候选组可以表达任务候选关系,但不一定知道企业数据权限。前端按钮隐藏也可以被绕过。最终授权必须在服务端完成。
十六、Job Executor为什么重要
Timer、异步 Service Task 等不会一直占用请求线程。引擎持久化 Job,Job Executor 获取后执行:
flowchart TD
A["Execution到达异步/Timer边界"] --> B["持久化Job并提交"]
B --> C["Job Executor定期获取到期Job"]
C --> D["锁定/抢占Job"]
D --> E["在新线程和新事务中执行"]
E --> F{"成功还是失败"}
F -->|"成功"| G["推进Execution并删除Job"]
F -->|"失败"| H["记录异常、减少重试或进入死信语义"]不同 Activiti 版本的 Job 表和死信能力不同。生产必须监控最老 Job、失败次数、锁超时和执行吞吐。
十七、版本差异必须先确认
Activiti 5
大量传统项目仍使用。API 思路与表结构是很多教程基础,但旧版依赖、Spring 集成和安全能力要评估。
Activiti 6
引擎内部和 BPMN 模型能力有较大演进,仍是常见单体嵌入式工作流选择。不要假设 Activiti 5 的内部类和迁移脚本完全适用。
Activiti 7
更强调云原生和应用服务/API 集成方向,依赖和使用方式与传统 5/6 教程可能不同。具体社区状态、模块和兼容性应查看项目锁定版本的官方仓库与发布说明。
Flowable
由 Activiti 社区历史分支演进而来,API 和表名有相似之处,但功能和实现已独立发展。Flowable 的 API、配置和文档不能无验证复制到 Activiti。
Camunda 7/8
Camunda 7 与早期 Activiti 有历史渊源,但已独立发展;Camunda 8/Zeebe 是不同架构。流程概念可比较,运行时、API、表和迁移不能互换。
选择时看:项目存量、JDK/Spring Boot兼容、活跃维护、BPMN能力、迁移工具、运维模型、社区和团队经验,不要只看名称相似。
十八、完整学习路线
第1阶段:总览和选型
当前页面必须掌握:对象关系、引擎分层、Service、Runtime/History、首个流程。
再阅读:工作流总览与选型。
第2阶段:BPMN可执行建模
重点:Event、Task、Gateway、Sequence Flow、SubProcess、Call Activity、Multi-instance、Listener、同步/异步和模型测试。
第3阶段:任务生命周期
重点:assignee/candidate、claim、转办、委派、complete事务、变量、权限、幂等、历史和列表性能。
第4阶段:网关与Execution
重点:排他、并行、包容、事件网关,Split/Join、变量类型和卡网关排查。
第5阶段:业务一致性
重点:同库事务、跨库命令、XA/JTA、Outbox/Inbox、事务消息、Saga、幂等、补偿和对账。
第6阶段:部署与版本
重点:Deployment、ProcessDefinition、版本生成、按 key/ID 启动、旧实例、缓存、挂起、删除、灰度和回退。
第7阶段:商业训练与验收
最后再看:Activiti面试题。面试页用于短回答,不替代原理页。
十九、生产项目必须补的能力
| 能力 | 最低要求 |
|---|---|
| 业务关联 | businessKey、processInstanceId、definitionId和binding |
| 权限 | assignee/candidate + 数据权限 + 操作权限 |
| 幂等 | requestId、task动作、Delegate、事件和消费唯一约束 |
| 事务 | 明确同库本地事务或跨库最终一致方案 |
| 版本 | 新旧定义、旧Delegate和变量兼容矩阵 |
| 异步 | Job重试、超时、死信/人工恢复和幂等 |
| 可观测性 | Task停留、Job失败、流程年龄、对账差异 |
| 数据生命周期 | Runtime容量、History归档、审计保留 |
| 修复 | 公共API、补偿、对账和受审计人工处置 |
| 安全 | 模型发布权限、表达式白名单、敏感变量治理 |
二十、最小排查路线
flowchart TD
A["确认环境、tenant、businessKey和时间"] --> B["查processInstanceId和definitionId"]
B --> C{"Runtime实例是否存在"}
C -->|"存在"| D["查Task、Execution、Variable、Job和订阅"]
C -->|"不存在"| E["查HistoricProcessInstance和结束原因"]
D --> F["下载实例对应版本BPMN"]
E --> F
F --> G["对照业务状态、审批记录和Outbox"]
G --> H["根据事务、权限、变量、网关或异步层定位"]不要首先重启或改数据库。重启可能让 Job 继续执行并改变现场,直接改表则可能进一步破坏执行树。
二十一、常见误区
| 误区 | 准确结论 | 后果 |
|---|---|---|
| Activiti就是画流程图 | BPMN会被解析为Execution、Task和Job | 图能看但不会运行/排障 |
| Activiti是完整审批系统 | 表单、权限、业务状态、通知仍需开发 | 越权和数据混乱 |
| ProcessDefinition等于Deployment | 一次部署可含多个定义 | 版本和查询理解错误 |
| Task等于流程实例 | 一个实例可先后/并行产生多个Task | 列表和状态模型错误 |
| Running等于有Task | 可能等待Job、消息、Timer或Join | 无Task就误判结束 |
| 流程变量可存完整业务对象 | 变量不是业务数据库 | 表膨胀、序列化和泄密 |
| complete就是删任务 | 它推进Execution并执行后续节点 | 不理解事务回滚 |
| 新定义覆盖旧实例 | 旧实例绑定原definitionId | 版本兼容事故 |
| @Transactional自动覆盖跨库 | 本地事务只管理自己的资源 | 双写不一致 |
| 直接改ACT表最快 | 会绕过引擎关联和缓存 | 流程损坏 |
二十二、面试标准回答
Activiti是什么
Activiti是BPMN工作流引擎。它把BPMN XML部署成版本化ProcessDefinition,业务启动时创建ProcessInstance和Execution,运行到User Task时生成待办,完成任务后推进Execution、计算网关,并将当前状态保存在Runtime表、轨迹保存在History表。业务系统仍负责业务表、权限、表单、通知和最终一致性。
Activiti一次流程怎样运行
RepositoryService先部署BPMN并生成定义;RuntimeService按key或definitionId启动实例,根Execution从Start Event沿Sequence Flow推进;到User Task后创建Task和Identity Link并持久化等待;用户经服务端权限校验后由TaskService完成任务,Execution继续经过网关、Service Task、异步Job或事件;到End后清理Runtime并保留History。
工作流和状态机怎么选
简单、短时、高吞吐的领域状态转换优先状态机;跨多人多角色、需要待办已办、并行会签、超时等待、历史审计和可视化版本的长流程适合工作流。任务调度解决定时批处理,Saga解决跨服务本地事务与补偿,它们可以与工作流组合而不是互相替代。
Runtime和History表有什么区别
Runtime表保存当前仍活动的Execution、Task、Variable和Job;Task完成或流程结束后相应运行记录会清理。History表按配置保存已发生实例、Activity、Task和变量轨迹。因此待办查Runtime,已办和轨迹查History并结合业务审批记录,不能一直查ACT_RU_TASK。
Activiti能替代业务系统吗
不能。Activiti只协调过程,不是领域事实、组织权限、表单、附件、消息和外部系统的事实源。业务表保存业务状态和数据,流程通过businessKey/processInstanceId关联;跨库和外部调用还要使用本地事务、Outbox、幂等、补偿和对账。
二十三、学习实验与验收
- 部署最小 BPMN,查询 deploymentId、definitionId、key 和 version。
- 启动两个 businessKey 不同的实例,证明一个定义可创建多个实例。
- 查询 Runtime Task、Execution 和 Variable,画出关系。
- claim 并 complete 一个 Task,观察 Runtime Task 消失和 Historic Task 生成。
- 把 approved 从 Boolean 改成 String,观察网关变量契约问题。
- 部署同 key V2,证明旧实例仍绑定 V1,新实例通常使用新激活版本。
- 创建异步 Service Task,观察 Job 与原请求事务的区别。
- 同库故障注入,证明业务表和 ACT 表共同回滚。
- 构造 Outbox 重复发送,使用 Inbox 唯一约束去重。
- 根据 businessKey、processInstanceId、definitionId 完成一次流程卡住排查。
验收时必须能回答:
- Deployment、ProcessDefinition、ProcessInstance、Execution和Task各是什么?
- 为什么User Task等待不需要占用一个Java线程?
- complete为什么不只是删除Task?
- Runtime和History为什么分开?
- Job Executor为什么是新线程/新事务?
- businessKey和processInstanceId怎样双向关联?
- 候选组为什么不能替代业务数据权限?
- 新版本为什么不自动迁移旧实例?
- 同库事务和跨库最终一致怎样选择?
- 卡住时为什么必须先确认实例绑定的definitionId?
