☰
Activiti6 中文用户指南实战:从建表到流程流转的避坑手册
2026/10/15 10:22:57 网站建设 项目流程

简介:Activiti6 中文用户指南面向 Java 开发者、工作流引擎学习者与业务流程设计人员,帮助读者系统掌握基于 BPMN 2.0 的开源工作流管理系统的配置与使用。资源包内含 1 个 docx 文档,压缩包约 4.22MB,内容围绕官方 6.0.0 版用户指南整理,涵盖系统要求、快速入门、ProcessEngine 与数据库配置、Job Executor、邮件服务器与历史记录配置、Process Engine API 与服务、查询与变量、单元测试、Spring 集成与 Spring Boot、BAR 部署与版本控制,以及 BPMN 2.0 构造中的事件定义、边界事件、中间捕获与投掷事件等模块。目录结构完整,便于按章节检索学习,可帮助读者快速搭建演示环境、理解流程引擎核心组件,并对照示例完成流程建模、部署与执行。目前已有 1816 人学习下载,适合需要查阅中文文档、排查配置问题或深入 BPMN 元素用法的开发者参考。

1. 拿到一份 Activiti6 中文用户指南,先别急着翻目录

很多人第一次接触 Activiti6,是因为接手了一个已经跑了两三年的审批系统,流程定义文件堆了几十个,前任留下的文档只有一份 Activiti6 中文用户指南.docx。打开一看,目录从「流程引擎配置」一路排到「历史数据查询」,每一节都像能看懂,合上文档却不知道从哪下手。这份指南真正的价值不在于让你背 API,而在于它把 BPMN 2.0 的语义和 Activiti6 的 Java 实现之间的映射关系讲清楚了——流程定义怎么部署、任务怎么流转、网关怎么分支、监听器在哪个生命周期节点触发,这些才是落地时天天要碰的东西。

它适合两类人:一类是要把 Activiti6 嵌进现有 Spring 项目、需要知道引擎启动和事务边界怎么配的后端;另一类是流程设计完发现跑不通、需要对照 BPMN 规范和引擎行为排查的开发者。如果你只是想要一个能画流程图然后点「发起」的轻量工具,Activiti6 的体量和学习曲线可能并不划算。但如果你面对的是多级审批、会签、驳回重提、动态加签这类真实业务,这份指南里关于任务查询、流程变量、网关条件的章节,基本就是你未来半年反复回查的内容。

2. 把 Activiti6 跑起来:从建表到第一个流程实例

2.1 引擎依赖与数据库初始化

Activiti6 的核心是activiti-engine,它不绑定 Spring,但绝大多数项目会配合activiti-spring使用。先看最小依赖集合,Maven 里加这几项就够了:

<!-- activiti-engine 是核心,包含流程解析、执行、任务管理 --> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-engine</artifactId> <version>6.0.0</version> </dependency> <!-- activiti-spring 提供与 Spring 事务、Bean 的集成 --> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring</artifactId> <version>6.0.0</version> </dependency> <!-- 数据库驱动按实际使用的库选择,这里以 MySQL 为例 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>

版本上,Activiti6 的稳定版是 6.0.0,后续有 6.0.0.RC 之类但生产上少见。数据库驱动版本要和你的 MySQL 服务端匹配,5.1.x 对应 MySQL 5.7 及以下,8.0.x 需要改驱动类名并处理时区参数。依赖加完后,引擎不会自动建表,需要显式调用初始化。

2.2 用代码建表并启动一个空引擎

建表有两种方式:一是用activiti-engine自带的db/create目录下的 SQL 脚本手动执行,二是通过ProcessEngineConfiguration的databaseSchemaUpdate参数让引擎自动建。我一般用后者做本地开发,生产环境用前者,因为自动建表在多人协作时容易因为版本不一致出问题。

// 使用独立方式创建引擎配置,不依赖 Spring 容器 ProcessEngineConfiguration cfg = ProcessEngineConfiguration .createStandaloneProcessEngineConfiguration() // 数据库连接四要素 .setJdbcUrl("jdbc:mysql://127.0.0.1:3306/activiti_demo?useUnicode=true&characterEncoding=utf8") .setJdbcUsername("root") .setJdbcPassword("root") .setJdbcDriver("com.mysql.jdbc.Driver") // true 表示启动时检查表结构,缺失则创建,存在则比对 .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE); // buildProcessEngine 会触发建表逻辑,执行完数据库里会出现 ACT_ 开头的 25 张表 ProcessEngine engine = cfg.buildProcessEngine(); System.out.println("引擎名称:" + engine.getName());

DB_SCHEMA_UPDATE_TRUE的含义是「启动时检查并创建」,如果表已存在且结构一致就跳过。另外两个可选值是FALSE(不检查,表必须已存在)和DROP_CREATE(先删后建,只在测试用)。建表完成后你会看到ACT_RE_(仓库)、ACT_RU_(运行时)、ACT_HI_(历史)、ACT_ID_(身份)四类表,其中ACT_RU_系列在流程结束后会清空,ACT_HI_系列保留归档数据,这个区分对后面查历史任务很关键。

2.3 部署第一个 BPMN 文件并启动实例

流程定义文件是 XML,后缀.bpmn或.bpmn20.xml都行。下面是一个只有开始事件、一个用户任务、一个结束事件的最小流程:

<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:activiti="http://activiti.org/bpmn" targetNamespace="http://demo.activiti"> <process id="leaveProcess" name="请假流程"> <startEvent id="start" name="开始"/> <!-- 用户任务,assignee 指定办理人,实际项目里通常用变量动态指定 --> <userTask id="submitTask" name="提交申请" activiti:assignee="applicant"/> <sequenceFlow id="flow1" sourceRef="start" targetRef="submitTask"/> <endEvent id="end" name="结束"/> <sequenceFlow id="flow2" sourceRef="submitTask" targetRef="end"/> </process> </definitions>

部署和启动的代码:

// 获取仓库服务,负责流程定义的部署和查询 RepositoryService repositoryService = engine.getRepositoryService(); Deployment deployment = repositoryService.createDeployment() .name("请假流程部署") // addClasspathResource 从类路径读取,也可以 addInputStream 从任意流读取 .addClasspathResource("processes/leaveProcess.bpmn") .deploy(); // 获取运行时服务,负责启动实例和推进流程 RuntimeService runtimeService = engine.getRuntimeService(); // 第二个参数是流程变量,这里把申请人传进去,供后续任务使用 ProcessInstance instance = runtimeService .startProcessInstanceByKey("leaveProcess", Collections.singletonMap("applicant", "user001")); System.out.println("实例ID:" + instance.getId());

startProcessInstanceByKey里的 key 对应 XML 中<process id="leaveProcess">的 id,不是 name。部署时如果同一个 key 重复部署,引擎会生成新版本,默认启动时用最新版本。流程变量在启动时传入后,会存入ACT_RU_VARIABLE表,后续任务查询和网关条件都能引用。这里传的applicant变量并没有自动绑定到activiti:assignee,因为 assignee 写的是字面量applicant,如果要动态指定,需要写成activiti:assignee="${applicant}",这是新手最容易混淆的一点。

3. 任务流转与网关:把审批逻辑真正串起来

3.1 用户任务的查询、签收与完成

流程实例启动后,用户任务会出现在ACT_RU_TASK表里。查询任务用TaskService,最常用的两个方法是按办理人和按流程实例查:

TaskService taskService = engine.getTaskService(); // 按候选人查询:查询 assignee 为 user001 或候选组包含该用户的任务 List<Task> tasks = taskService.createTaskQuery() .taskAssignee("user001") .orderByTaskCreateTime().desc() .list(); for (Task task : tasks) { System.out.println("任务ID:" + task.getId() + ",名称:" + task.getName() + ",创建时间:" + task.getCreateTime()); } // 签收:把候选任务认领到自己名下,assignee 从 null 变为当前用户 // 如果任务已经指定了 assignee,这一步可以跳过 taskService.claim(taskId, "user001"); // 完成任务,第二个参数是完成任务时写入的流程变量 taskService.complete(taskId, Collections.singletonMap("approved", true));

claim的作用在多人候选场景下才明显:任务创建时 assignee 为空,候选组里有多个用户,谁先 claim 谁就成为办理人,其他人再查就看不到这个任务了。如果任务在 BPMN 里已经写死了 assignee,claim 会抛异常,因为引擎不允许修改已有办理人。complete之后,引擎会根据当前节点的出口连线决定下一步走向,如果后面是排他网关,就会读取变量approved的值来判断。

3.2 排他网关与条件表达式的写法

排他网关(exclusiveGateway)是审批流里用得最多的分支结构,它按顺序检查每条出口连线上的条件,走第一条为 true 的。条件用 UEL 表达式写,变量来自流程变量:

<exclusiveGateway id="decisionGateway" name="审批决策"/> <sequenceFlow id="flowApprove" sourceRef="decisionGateway" targetRef="archiveTask"> <!-- 条件表达式里直接引用流程变量名,不需要加前缀 --> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approved == true}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flowReject" sourceRef="decisionGateway" targetRef="rejectTask"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approved == false}]]> </conditionExpression> </sequenceFlow>

这里有几个参数层面的坑。第一,approved变量在complete时传入的是 Boolean 类型,表达式里写approved == true可以,写approved == 'true'就会因为类型不匹配恒为 false。第二,如果所有条件都不满足且没有默认连线,引擎会抛ActivitiException: No outgoing sequence flow ... could be selected,所以排他网关最好设一条default属性指向兜底连线。第三,条件表达式里的变量名如果拼错,不会报错,只会静默走默认分支或不走,排查时要在ACT_RU_VARIABLE里确认变量确实写入了。

3.3 会签与多实例任务的配置要点

会签在 BPMN 里用多实例(multiInstance)实现,分并行和串行两种。并行会签是所有人同时收到任务,串行是一个接一个。下面是一个并行会签的配置:

<userTask id="countersignTask" name="会签审批"> <!-- 多实例类型:parallel 并行,sequential 串行 --> <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="approvers" activiti:elementVariable="approver"> <!-- 完成条件:当 nrOfCompletedInstances 达到 nrOfInstances 时结束 --> <completionCondition> <![CDATA[${nrOfCompletedInstances == nrOfInstances}]]> </completionCondition> </multiInstanceLoopCharacteristics> </userTask>

activiti:collection指定一个集合变量,activiti:elementVariable是每次迭代时集合元素的变量名,任务里可以用${approver}动态指定办理人。nrOfInstances是总实例数,nrOfCompletedInstances是已完成数,这两个是引擎内置变量,不用自己传。如果要做「一人否决即通过」的会签,把完成条件改成${nrOfCompletedInstances/nrOfInstances >= 0.5}之类的比例即可。注意并行会签在ACT_RU_TASK里会生成多条任务记录,查询时要按流程实例 ID 过滤,否则会和其他流程的任务混在一起。

4. 监听器与事件:在流程节点上挂业务逻辑

4.1 执行监听器与任务监听器的区别

监听器是 Activiti6 里把业务代码挂到流程上的主要手段,分执行监听器(ExecutionListener)和任务监听器(TaskListener)。执行监听器挂在流程、活动、连线上,监听 start、end、take 三类事件;任务监听器只挂在用户任务上,监听 create、assignment、complete、delete 四类事件。选型上,如果要在节点进入时写业务表、发消息,用执行监听器的 start;如果要动态设置办理人、在任务完成前做校验,用任务监听器。

// 执行监听器实现,监听流程节点开始和结束 public class ProcessStartListener implements ExecutionListener { @Override public void notify(DelegateExecution execution) { // execution.getEventName() 返回 start 或 end String eventName = execution.getEventName(); // 获取流程变量 Object applicant = execution.getVariable("applicant"); System.out.println("节点 " + execution.getCurrentActivityId() + " 触发事件 " + eventName + ",申请人:" + applicant); } }
// 任务监听器实现,在任务创建时动态指定办理人 public class TaskCreateListener implements TaskListener { @Override public void notify(DelegateTask delegateTask) { // 从流程变量里取上级,设置为任务办理人 String manager = (String) delegateTask.getVariable("manager"); delegateTask.setAssignee(manager); } }

XML 里挂载监听器:

<userTask id="approveTask" name="上级审批"> <extensionElements> <!-- 任务监听器:event 指定 create/assignment/complete/delete --> <activiti:taskListener event="create" class="com.demo.listener.TaskCreateListener"/> </extensionElements> </userTask>

监听器类必须有无参构造函数,因为引擎通过反射实例化。如果监听器里要调用 Spring Bean,直接 new 出来的对象拿不到容器里的依赖,常见做法是用activiti:delegateExpression="${myListenerBean}"代替class,让引擎从 Spring 容器取。这个区别在本地测试时不容易发现,一上 Spring 环境就报空指针,属于典型的血泪经验。

4.2 用表达式动态指定办理人

除了监听器,BPMN 里也可以直接用 UEL 表达式指定 assignee 和 candidateUsers:

<!-- 从流程变量里取办理人 --> <userTask id="task1" name="审批" activiti:assignee="${approver}"/> <!-- 候选用户用逗号分隔,也可以引用集合变量 --> <userTask id="task2" name="候选审批" activiti:candidateUsers="${candidateUserList}"/> <!-- 候选组,组 ID 从变量取 --> <userTask id="task3" name="组审批" activiti:candidateGroups="${groupIds}"/>

表达式在任务创建时求值,如果变量不存在,assignee 会被设为 null,任务进入候选状态。这里有个容易翻车的点:candidateUsers传集合时,引擎内部会拼成逗号分隔的字符串存到ACT_RU_IDENTITYLINK表,如果集合里有 null 元素,查询时会报错。我一般会在传变量前先过滤掉 null 和空字符串。

4.3 历史数据查询与流程追踪

流程跑完后,运行时表清空,要查历史得用HistoryService。Activiti6 默认的历史级别是audit,记录流程实例、活动实例、任务实例和变量,但不记录变量更新明细。如果要追踪每次变量变化,需要把historyLevel调到full,代价是ACT_HI_DETAIL表会快速增长。

HistoryService historyService = engine.getHistoryService(); // 查询某个流程实例的历史活动节点,按开始时间排序 List<HistoricActivityInstance> activities = historyService .createHistoricActivityInstanceQuery() .processInstanceId(instanceId) .orderByHistoricActivityInstanceStartTime().asc() .list(); for (HistoricActivityInstance activity : activities) { System.out.println("节点:" + activity.getActivityName() + ",类型:" + activity.getActivityType() + ",开始:" + activity.getStartTime() + ",结束:" + activity.getEndTime() + ",耗时:" + activity.getDurationInMillis() + "ms"); }

getDurationInMillis返回的是毫秒数,如果节点还没结束,这个值是 null,直接做算术会空指针。查历史任务用createHistoricTaskInstanceQuery,查历史变量用createHistoricVariableInstanceQuery,后者在audit级别下只保留最终值,不保留中间过程。如果业务需要「谁在什么时候把金额从 100 改成 200」这种审计,必须上full级别,并且提前规划好历史表的清理策略,否则一年下来ACT_HI_DETAIL能到千万行。

5. 避坑与排查:Activiti6 落地时最容易翻车的五件事

5.1 流程定义缓存导致改了 BPMN 不生效

现象:修改了 BPMN 文件重新部署,启动实例后走的还是旧逻辑。原因是 Activiti6 默认开启流程定义缓存,processDefinitionCacheLimit默认是 -1 表示不限制,引擎会把解析后的ProcessDefinitionEntity缓存在内存里,部署新版本后旧缓存不一定立即失效。解决方式是在ProcessEngineConfiguration里设置setProcessDefinitionCacheLimit(0)关闭缓存,或者每次部署后调用repositoryService.getProcessDefinition确认版本号。生产环境不建议关缓存,更稳妥的做法是部署时用deploymentName加时间戳区分,并确认ACT_RE_PROCDEF表里 VERSION 字段递增。

5.2 异步任务与事务边界不清导致数据不一致

现象:在任务监听器里写了业务表,流程完成后业务表有数据但流程历史里任务状态不对,或者反过来。原因是 Activiti6 的complete操作和监听器里的业务代码默认在同一个事务里,如果监听器抛异常,流程回滚但业务表可能因为用了独立事务而没回滚。解决方式是把业务写操作放到流程引擎事务内,用SpringTransactionInterceptor保证同一个DataSource;如果业务必须独立事务,就在监听器里发消息,由消费者异步处理,并做好幂等。另外,activiti:async="true"的节点会走ACT_RU_JOB表,由AsyncExecutor调度,默认不开启,需要显式配置setAsyncExecutorActivate(true)。

5.3 表达式里的变量类型不匹配

现象:排他网关条件approved == true永远走 false 分支。原因是complete时传的approved是字符串"true"而不是布尔true,UEL 比较时类型不同直接返回 false,且不报错。排查方式是在ACT_RU_VARIABLE表里查TYPE_字段,boolean对应BooleanType,string对应StringType。解决方式是在 Java 侧统一用Boolean类型传参,或者在表达式里做显式转换${approved == 'true'},但后者一旦变量类型变了又会失效,不如从源头统一。

5.4 多实例会签的完成条件写错导致提前结束

现象:并行会签设置了 3 个人,第一个人完成后整个会签节点就结束了。原因是completionCondition写成了${nrOfCompletedInstances > 0}或者漏写了条件,引擎默认在nrOfCompletedInstances == nrOfInstances时才结束,但如果条件表达式恒为 true,就会提前触发。排查时看ACT_RU_EXECUTION表里该节点的IS_ACTIVE和NR_OF_COMPLETED_INSTANCES字段。解决方式是条件里明确用${nrOfCompletedInstances == nrOfInstances},如果要做比例通过,用${nrOfCompletedInstances/nrOfInstances >= 0.5},注意除法在 UEL 里是整数除法,3 个人里 2 个完成是 0.66,1 个完成是 0.33,不会出现浮点精度问题。

5.5 历史表膨胀拖慢查询

现象:系统跑了一年,查历史任务越来越慢,ACT_HI_ACTINST和ACT_HI_TASKINST单表过千万行。原因是默认历史级别audit会记录所有活动实例,包括网关、连线这些不产生业务价值的节点。解决方式有三个:一是把historyLevel从audit降到activity,只记录流程实例和活动实例,不记录任务和变量;二是定期归档,把半年前的历史数据迁到备份表;三是在查询时强制走索引,ACT_HI_PROCINST的PROC_DEF_ID_和START_TIME_上建联合索引。我一般会在项目初期就定好归档策略,而不是等慢了再补,因为历史数据一旦产生,清理的窗口期很难找。

6. 把 Activiti6 用稳的一个习惯:先看表再写代码

6.1 用 SQL 直接观察引擎状态

Activiti6 的所有运行时状态都在数据库里,遇到流程不按预期走,与其在 Java 里打断点,不如直接查表。下面几条 SQL 是我排查时最常用的:

-- 查当前流程实例的活跃节点,IS_ACTIVE_ = 1 表示正在执行 SELECT e.ID_, e.ACT_ID_, e.IS_ACTIVE_, e.IS_CONCURRENT_, e.PARENT_ID_ FROM ACT_RU_EXECUTION e WHERE e.PROC_INST_ID_ = '你的流程实例ID'; -- 查当前实例的所有任务,包括未签收的 SELECT t.ID_, t.NAME_, t.ASSIGNEE_, t.CREATE_TIME_, t.TASK_DEF_KEY_ FROM ACT_RU_TASK t WHERE t.PROC_INST_ID_ = '你的流程实例ID'; -- 查流程变量及其类型,TYPE_ 字段决定表达式比较行为 SELECT v.NAME_, v.TYPE_, v.TEXT_, v.LONG_ FROM ACT_RU_VARIABLE v WHERE v.PROC_INST_ID_ = '你的流程实例ID'; -- 查历史活动,按开始时间排序,看流程实际走了哪些节点 SELECT h.ACT_ID_, h.ACT_NAME_, h.ACT_TYPE_, h.START_TIME_, h.END_TIME_, h.DURATION_ FROM ACT_HI_ACTINST h WHERE h.PROC_INST_ID_ = '你的流程实例ID' ORDER BY h.START_TIME_ ASC;

ACT_RU_EXECUTION是理解 Activiti6 执行模型的关键表。一个流程实例启动后,至少有一条PROC_INST_ID_等于自身 ID 的执行记录,遇到并行网关或会签会分裂出子执行,PARENT_ID_指向父执行。IS_ACTIVE_为 1 表示该执行正在等待某个节点完成,为 0 表示它已经走到等待状态但当前不活跃。很多「流程卡住」的问题,查这张表就能定位到是哪个执行没往下走。

6.2 一个验证流程定义是否正确的检查清单

在把 BPMN 部署到生产之前,我习惯按下面这个顺序过一遍,能挡掉八成低级错误:

检查项检查方式常见问题
流程 key 唯一性查ACT_RE_PROCDEF的KEY_和VERSION_重复部署导致版本混乱
开始事件出口确认 startEvent 有且仅有一条出口连线多条出口导致解析失败
排他网关默认分支检查default属性是否指向兜底连线条件全不满足时抛异常
用户任务办理人确认 assignee 或 candidateUsers 表达式变量存在变量未传导致任务无人认领
多实例完成条件确认completionCondition逻辑正确提前结束或永不结束
监听器类路径确认 class 全限定名正确且有默认构造部署时报 ClassNotFound
历史级别确认historyLevel与业务审计需求匹配级别过低查不到变量变更

这个清单不是走形式,每一条都对应我或身边同事真实踩过的坑。尤其是排他网关的默认分支和多实例完成条件,出问题时流程不会报错,只会静默走错分支或者卡住,排查成本远高于部署前花五分钟检查。

6.3 版本升级与兼容性的一点经验

Activiti6 到 Activiti7 的 API 变化不小,IdentityService和FormService在 7 里被移除或重构,如果项目里用了这些,升级时改造成本不低。我的习惯是:如果现有系统跑的是 Activiti6 且没有硬性升级需求,不要为了追新而升,因为流程引擎的稳定性比新特性重要得多。如果确实要升,先在测试环境用同一批 BPMN 文件跑一遍全流程,重点验证网关条件、监听器、多实例和历史查询这四块,它们是最容易在版本间行为不一致的地方。另外,Activiti6 的activiti-spring-boot-starter在 Spring Boot 2.x 下需要额外处理SecurityAutoConfiguration的冲突,这个在官方指南里不一定写,但实际集成时绕不开。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询