
简介Activiti 5.17.0 官方工作流引擎资源包面向需要实现企业级流程自动化的 Java 开发人员、流程架构师与系统运维者可基于 BPMN 2.0 搭建流程模型并借助引擎完成流程实例调度、任务分配与状态查询适用于审批、工单、项目管理等常见场景。压缩包为 zip 格式整体约 69.35MB内容涵盖官方发布的 jar 运行文件、源代码与离线手册包内文件组织清晰便于按需检索既可直接引入项目运行也可对照源码研究流程引擎核心机制。目前已有 202 人学习浏览有助于从入门到进阶掌握 Activiti。该版本重点关注流程定义与模型设计、引擎 API 使用、任务与分支并发处理、动态表单关联、事件监听与自定义行为等知识点同时可学习与 Spring、Hibernate 的整合方式以及 REST API、事务管理、数据库持久化等工程化能力使开发者能够快速将流程引擎融入业务系统。借助源码与手册开发者能深入理解 Activiti 5.17.0 的整体结构为后续基于该框架的业务流程二次开发提供可靠参考。1. activiti-5.17.0 到底是个什么版本先给一句判断如果你所在的团队接手了一套 2015 年前后的审批系统大概率会看到一个老熟人activiti-5.17.0。它没有 7.x 的云原生概念也不带 Spring Boot 的自动配置但核心能力一个不少流程部署、任务流转、会签、驳回、条件分支、历史查询全部撑得住。真正让人翻车的地方集中在三处和 Spring 版本的配合、数据库表名大小写、以及 BPMN 文件里的命名空间细节。这篇笔记会顺着“搭环境 → 跑通一条流程 → 审批流转 → 避坑 → 批量处理技巧”的顺序讲透每一段都给出能直接复制的代码和参数。适合两类人看刚接手存量系统、被历史代码追着跑的新人以及正在评估“老版本值不值得继续投入”的架构师。2. 先把引擎跑起来依赖、配置与建表动作Activiti 5.17.0 的引擎核心是一个重量级对象 ProcessEngine它负责管理所有的 Service部署、运行时、任务、历史、身份所有操作都从它身上拿。这一章不碰 Spring先把“裸引擎”跑起来目的是确认依赖、数据库和建表策略这三件事都正确。这一步通了后面接 Spring 还是接别的容器都只是装配问题。2.1 依赖怎么加两个坐标别加多先看 Maven 依赖。5.17.0 的坐标和 7.x 不一样核心是这个dependency groupIdorg.activiti/groupId artifactIdactiviti-engine/artifactId version5.17.0/version /dependency !-- 如果要接 Spring 3/4再加这个不要重复引入 engine -- dependency groupIdorg.activiti/groupId artifactIdactiviti-spring/artifactId version5.17.0/version /dependency这里有个常见误操作有人为了“保险”把 activiti-engine、activiti-spring、activiti-bpmn-converter 一次性全引进来结果 classpath 里出现同一类的多个版本启动时直接 NoSuchMethodError。5.17.0 的依赖管理没有 7.x 那么完善engine 自己带了模型解析、BPMN 转换、MyBatis 映射正常业务不需要额外加解析模块。参数说明activiti-engine 内部会传递依赖 mybatis 和 spring-expression如果你的项目里已经用了另一个版本的 mybatis这里大概率会冲突。我一般会在引入后先跑一次mvn dependency:tree -Dincludesorg.activiti看实际的依赖树确认没有两个版本的 engine 并存。这个习惯能省掉后面一小时的排查时间。2.2 引擎配置数据库连接和建表策略5.17.0 提供了独立的配置类 ProcessEngineConfiguration可以通过 Java 代码直接构建引擎这是最小化验证的最佳路径不依赖 web 容器、不依赖 Spring 上下文。import org.activiti.engine.ProcessEngine; import org.activiti.engine.ProcessEngineConfiguration; public class EngineBootstrap { public static void main(String[] args) { ProcessEngineConfiguration config ProcessEngineConfiguration .createStandaloneProcessEngineConfiguration() // MySQL 连接串注意 UTF-8 参数 .setJdbcUrl(jdbc:mysql://localhost:3306/activiti_test?useUnicodetruecharacterEncodingutf8) .setJdbcDriver(com.mysql.jdbc.Driver) .setJdbcUsername(root) .setJdbcPassword(root) // true 没有表就自动创建false 不建表drop-create 每次删了重建 .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE) // 历史级别full 记录最全none 完全不记录 .setHistoryLevel(ProcessEngineConfiguration.HISTORY_FULL); ProcessEngine processEngine config.buildProcessEngine(); System.out.println(Engine name: processEngine.getName()); } }逻辑说明createStandaloneProcessEngineConfiguration()创建的是一个不依赖事务管理器、不依赖数据源的“独立引擎配置”适合工具类、命令行脚本、单元测试。如果直接接 Spring应该换成createProcessEngineConfigurationFromResource(activiti.cfg.xml)或者用 Spring 的SpringProcessEngineConfiguration但这不影响建表逻辑。参数说明setDatabaseSchemaUpdate有三个值生产环境我强烈建议用false并手工执行官方 SQL 脚本而不是让引擎每次启动都去探测表结构。setHistoryLevel默认是HISTORY_AUDIT记录流程实例、活动实例、任务、变量但跳过流程变量更新明细如果不需要历史数据改成HISTORY_NONE能显著减少写入量。这里先保持 FULL方便后面的章节验证历史查询。2.3 验证引擎真的初始化成功引擎启动成功不等于表都建对了。5.17.0 默认会创建一组以 ACT_ 开头的表分布在几个域里ACT_RE_流程定义和流程模型、ACT_RU_运行时、ACT_HI_历史、ACT_ID_身份信息、ACT_GE_通用数据。验证方式直接查数据库SELECT table_name FROM information_schema.tables WHERE table_schema activiti_test AND table_name LIKE ACT\_%;正常情况下列表里应该同时出现 ACT_RE_PROCDEF、ACT_RU_TASK、ACT_HI_PROCINST、ACT_GE_BYTEARRAY 这些核心表。如果一张表都没有先确认数据库账号有建表权限如果只建了一半就停下大概率是字符集或表名大小写问题这部分在第 5 章细说。这一步还有一个容易被忽略的点buildProcessEngine()默认创建的引擎 id 是default。如果同进程里再次调用同样代码会得到同一个引擎实例单例缓存而不是重新建一遍。你可以在跑完代码后调用System.out.println(ProcessEngines.getProcessEngines().keySet());看到输出里有default说明引擎已经注册到全局缓存。后续代码里直接ProcessEngines.getDefaultProcessEngine()就能拿到同一个实例不需要重新配置。这也是为什么很多基于 5.17.0 的老项目工具类里到处是ProcessEngines.getDefaultProcessEngine()的原因。3. 让第一个流程实例跑通BPMN、部署与启动参数引擎起来后下一件事是部署一条真实流程。这一章用手写 BPMN 文件的方式走完全流程写文件 → 部署 → 查询流程定义 → 启动实例。整个过程里最容易出错的不是 Java 代码而是 XML 文件里的命名空间和表达式写法。3.1 一个能直接用的请假审批流程 BPMN下面这个文件是略去布局信息的最小 BPMN 2.0 流程包含一个开始事件、两个用户任务、一个排他网关、一个结束事件。注意文件后缀用.bpmn20.xml这是 5.17.0 部署时识别最稳的命名方式。?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:activitihttp://activiti.org/bpmn targetNamespacehttp://example.com/leave process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstart name开始/ sequenceFlow idflow1 sourceRefstart targetRefapply/ userTask idapply name提交请假申请 activiti:assignee${submitter}/ sequenceFlow idflow2 sourceRefapply targetRefapprove/ userTask idapprove name审批 activiti:assignee${approver}/ sequenceFlow idflow3 sourceRefapprove targetRefgateway/ exclusiveGateway idgateway name是否通过/ sequenceFlow idflowPass sourceRefgateway targetRefend conditionExpression xsi:typetFormalExpression${approved true}/conditionExpression /sequenceFlow sequenceFlow idflowReject sourceRefgateway targetRefapply conditionExpression xsi:typetFormalExpression${approved false}/conditionExpression /sequenceFlow endEvent idend name结束/ /process /definitions两个新手必踩的点第一activiti:前缀的命名空间声明不能漏activiti:assignee依赖它漏了会直接部署失败第二${approved true}这种表达式是 JUEL 语法布尔值直接比较就行不用加引号。字符串比较才需要引号比如${approved true}。我把这份文件放在src/main/resources/diagrams/leave.bpmn20.xml下接下来用它做部署测试。3.2 部署流程定义关键在 latestVersion部署动作由 RepositoryService 负责。部署的本质是把 BPMN 文件打包成一个 Deployment 对象入库引擎再从中解析出 ProcessDefinition。同一个流程如果部署两次会生成两个版本但 key 相同。RepositoryService repositoryService processEngine.getRepositoryService(); // 部署addClasspathResource 可以多次调用支持一个包带多个文件 Deployment deployment repositoryService.createDeployment() .addClasspathResource(diagrams/leave.bpmn20.xml) .name(请假流程) .deploy(); // 查询最新版本的流程定义 ProcessDefinition definition repositoryService.createProcessDefinitionQuery() .processDefinitionKey(leaveProcess) .latestVersion() .singleResult(); System.out.println(流程定义ID: definition.getId()); System.out.println(版本号: definition.getVersion());逻辑说明processDefinitionKey对应 BPMN 文件里process idleaveProcess这个 id不是文件名。latestVersion()会按 key 分组取版本号最大的那个这个限定在重复部署后尤其重要否则singleResult()会因为查到多行而抛异常。deployment.getId()和definition.getId()是两回事前者是部署包 id后者是流程定义 id格式通常像leaveProcess:1:某个数字。启动流程实例时用的是 key 或定义 id不是部署 id。3.3 启动流程实例businessKey 和 variables 的配合流程定义是“图纸”流程实例是“按图纸跑起来的具体业务”。启动时通常要传两个东西业务单据编号businessKey和流程变量variables。业务上最典型的例子是“工单号或请假单号”。RuntimeService runtimeService processEngine.getRuntimeService(); IdentityService identityService processEngine.getIdentityService(); MapString, Object variables new HashMap(); variables.put(submitter, user01); variables.put(approver, user02); variables.put(days, 2); ProcessInstance processInstance runtimeService.startProcessInstanceByKey( leaveProcess, LEAVE-2024-001, variables); System.out.println(流程实例ID: processInstance.getId()); System.out.println(业务编号: processInstance.getBusinessKey());参数说明startProcessInstanceByKey有三个重载版本第二个参数是 businessKey第三个是 variables。businessKey 建议填业务单据的唯一编号不要填自增数字后续用historicProcessInstanceQuery().processInstanceBusinessKey(LEAVE-2024-001)可以精确找回这个实例关联的所有历史数据。这里的 variables 会直接影响流程走向submitter决定第一个用户任务的办理人approver决定审批任务的办理人。如果漏了这两个变量任务是能创建出来的但 assignee 字段是空的这帮任务不会出现在任何人的待办列表里只能靠taskQuery().taskUnassigned()捞出来——这是第 5 章要展开讲的一个经典坑。还有一点要注意identityService.setAuthenticatedUserId(user01)这行在某些团队代码里会出现作用是为历史记录里“发起人”字段赋值不是流程执行的必需条件。老系统里常见但无关流程能否跑通建议把它和业务操作解耦。4. 审批流转的三种常用姿势任务、变量、会签与驳回流程实例启动后真正的业务量都在“审批”上。这一章覆盖三个最常见诉求普通单人审批、条件网关分支、会签和驳回。5.17.0 在 API 上非常直白但表达式的细节决定流程走的对不对。4.1 待办任务查询与 complete 的完整链路审批人打开待办列表本质是查询 ACT_RU_TASK 表里 assignee 等于自己或者候选人是自己的任务。完成审批则有两条路直接 complete或先 setAssignee 再 complete。代码如下TaskService taskService processEngine.getTaskService(); // 查询 user02 的待办任务 ListTask tasks taskService.createTaskQuery() .taskAssignee(user02) .orderByTaskCreateTime() .desc() .list(); for (Task task : tasks) { System.out.println(task.getId() - task.getName()); } // 完成第一个任务并传递审批结果变量 Task task tasks.get(0); MapString, Object taskVars new HashMap(); taskVars.put(approved, true); taskService.complete(task.getId(), taskVars);逻辑说明taskAssignee(user02)查的是办理人已经明确分配给 user02 的任务。如果团队用候选人模式比如任务分配给一个角色组需要改成taskCandidateUser(user02)并且 complete 时引擎会校验 user02 是否是候选人。complete方法执行时引擎会顺着当前节点的出口 SequenceFlow 走遇到排他网关时用表达式的值决定走哪个分支。任务变量和流程变量是两套存储complete(taskId, vars)里传入的变量写入的是流程变量ACT_RU_VARIABLE不是任务本地变量。如果只想给当前任务附加数据而不影响流程走向应该用taskService.setVariableLocal(taskId, key, value)。我见到的错误大多是该用 local 的地方用了全局导致后续流程的表达式被不该存在的变量影响。4.2 排他网关的条件表达式类型决定写法排他网关ExclusiveGateway在 5.17.0 里是默认网关遇到多条出口线时会从上到下找第一个条件为 true 的线。上一章的请假流程里网关的两条线分别用approved true和approved false判断。这里是 JUEL 表达式类型很敏感变量类型正确写法常见错误Boolean${approved true}${approvedtrue}Integer${days 3}${days 3}String${auditType PASS}或${auditType eq PASS}${auditType PASS}null 判断${approver null}或${approver ! null}直接${approver}团队里最常见的玄学现场${approved}单独作为条件表达式当 approved 是 true 时确实为真但当它只是“存在但值为 null”时表达式会抛异常而不是走 false 分支。所以条件判断宁可写完整也不要省略比较对象。另一个习惯是把流程结果的枚举值如 “PASS”、“REJECT”用字符串变量传可读性比布尔值高但记得在 gateway 两条线上都写条件并在最后留一条无条件的默认线防止变量缺失时直接抛异常。4.3 会签节点与“驳回”的常见实现方式5.17.0 支持 BPMN 2.0 的多实例活动multiInstanceLoopCharacteristics会签就是靠它实现的。下面是一个 60% 通过即放行的多人会签节点userTask idmultiApprove name多人会签 activiti:assignee${currentApprover} multiInstanceLoopCharacteristics isSequentialfalse activiti:collection${approverList} activiti:elementVariablecurrentApprover completionCondition${nrOfCompletedInstances nrOfInstances * 0.6}/completionCondition /multiInstanceLoopCharacteristics /userTask参数说明approverList是启动流程时传入的 List 变量currentApprover是引擎在每次循环时自动生成的局部变量代表当前要处理的审批人isSequentialfalse表示并行会签每个任务同时生成而不是排着队来。completionCondition 是完成条件表达式默认全部处理完才算完成写了这个条件后达到 60% 就会提前结束。注意缺失场景如果启动流程时没传approverList或传的 List 为空这个节点运转时会报ActivitiException提示无法解析集合。这属于启动参数校验问题我会在启动流程前先用业务代码校验集合是否为空而不是等引擎运行到一半才炸。驳回在 5.17.0 里没有像 7.x 那样的专用 API最常见做法是我在第 3 章 BPMN 里展示的回退流审批网关条件为 false 时指向“重新申请”任务让提交人修改后再提交。简单、可追踪、历史记录完整比直接调用底层 jump 更安全。如果确认团队对底层 API 有把握也可以走runtimeService.createProcessInstanceModification()但不建议在核心业务里冒这个险。做出决定前先问自己审批记录要不要留痕驳回后要不要改表单数据网关回退在这两点上都占优势。5. 避坑5.17.0 最常见的五个翻车现场这一章写的每个坑都是真实会让项目半夜响电话的那种。按“现象 → 原因 → 解决”的格式展开顺序大致按从环境到代码再到运维排。5.1 Linux 下表名大小写导致引擎找不到表现象同一个应用在 Windows 开发机上跑得好好的部署到 Linux 测试环境后引擎启动报错提示找不到 ACT_HI_PROCINST 或 ACT_ID_GROUP数据源连接正常但建表永远不完整。原因MySQL 在 Linux 上默认区分表名大小写lower_case_table_names0而 5.17.0 的建表 SQL 里表名大小写混用部分语句用小写表名部分用大写。开发环境 MySQL 不区分大小写所以掩盖了问题。解决第一时间检查 MySQL 配置在[mysqld]段加lower_case_table_names1并重启数据库如果数据库已经建好需要先把旧表导出备份删除库后重建。对已经上线的库更稳妥的方式是手工执行官方 SQL 脚本建表然后把setDatabaseSchemaUpdate改成false让应用不再参与建表动作从源头避免大小写探测。5.2 中文乱码流程定义名、任务名全是问号现象BPMN 文件里写了中文名称部署后查询流程定义显示name字段是“???”任务节点名字在流程图里也乱码。原因串了两个环节。第一BPMN 文件本身不是 UTF-8 编码某些 Windows 编辑器会默认存成 GBK第二JDBC 连接没有显式声明字符集MySQL 会话使用默认 latin1入库时按单字节存转义符号。解决BPMN 文件统一保存为 UTF-8 无 BOM 格式编辑器右下角能看编码JDBC 连接串带上useUnicodetruecharacterEncodingutf8两个参数成对出现缺一个都不生效。注意 5.17.0 年代用的 MySQL 驱动是 5.1.x参数名必须这样写。改完连接串后已经乱码的数据需要重新部署流程历史数据不会自动修复。5.3 依赖冲突应用启动直接 NoSuchMethodError现象Web 应用引入 activiti-engine 5.17.0 后Spring 容器启动时报NoSuchMethodError或ClassNotFoundException堆栈指向org.springframework.expression或org.apache.ibatis。原因5.17.0 对传递依赖的版本管理很松散尤其 mybatis 和 spring-expression。项目里已有的 mybatis 3.4 与内置的 mapper 映射不兼容或者 Spring 4 的 expression 模块和引擎依赖的版本不一致都会在运行时爆发。解决跑mvn dependency:tree -Dincludesorg.activiti,org.mybatis,org.springframework看完整依赖树找到实际生效的版本号。常见做法是在项目的 dependencyManagement 里把 mybatis 锁定到 3.2.x5.17.0 适配较好的版本同时确认 Spring 版本在 3.x 或 4.x 的早期版本区间避免直接用 Spring 4.3 以上的新特性。如果项目已经用了新版本 mybatis且升级引擎不现实优先级应该是评估接口隔离而不是继续在依赖里硬压版本。5.4 待办任务永远查不到assignee 变量缺失现象流程实例启动了数据库里 ACT_RU_TASK 也有任务记录但taskAssignee(某用户)查询结果为空。再一查原表任务的 ASSIGNEE_ 字段是 NULL。原因BPMN 里写的activiti:assignee${submitter}用了表达式但启动流程时 variables 里没有 submitter 或 submitter 为 null。引擎不会在启动时校验变量是否存在只会在创建任务节点时把表达式求值结果写入 ASSIGNEE_ 字段求值为 null 时字段就是 NULL。NULL 的任务不会出现在任何人的待办列表里但流程会一直停在那个节点。解决在startProcessInstanceByKey之前用业务代码对必填流程变量做校验。我习惯写一个启动前的变量检查方法手动校验submitter、approver、approverList这些 BPMN 里引用到的 key缺一个就抛业务异常宁可在启动时拒绝也不要让流程默默停摆。如果线上已经出现这种任务补救方法是taskService.setAssignee(taskId, user02)手动补上办理人再让用户继续操作。5.5 历史表无限膨胀没有归档策略现象上线半年后ACT_HI_ACTINST 表数据量达到上千万行历史流程查询接口响应从毫秒级变成秒级数据库所在服务器 IO 频繁告警。原因5.17.0 引擎默认historyLevel是 audit对流程实例、活动实例、任务、变量全量记录且没有任何自动归档清理机制。很多业务系统上线后完全没处理历史表导致数据量线性增长。解决分三个动作处理。第一评估历史数据必要程度把setHistoryLevel调整到HISTORY_ACTIVITY或HISTORY_NONE减少写入量注意会丢失变量历史降级前要确认业务可接受第二定时清理三个月前的历史数据直接按时间删除 SQL-- 按时间删除已完成流程的活动实例记录 DELETE FROM ACT_HI_ACTINST WHERE END_TIME_ IS NOT NULL AND END_TIME_ 2024-01-01 00:00:00; -- 清理任务实例 DELETE FROM ACT_HI_TASKINST WHERE END_TIME_ IS NOT NULL AND END_TIME_ 2024-01-01 00:00:00;第三对还在运行的流程实例不要删否则历史流程视图会断链。清理脚本放到定时任务里跑建议先备份验证压测到目标数据量后再全量执行。我见过因为没备份直接跑删除结果误删未结束流程实例导致流程跳转断掉的情况这操作没有后悔药。6. 收尾技巧批量任务用事务隔离别让一个失败拖垮整批老系统发展到后面一定会遇到“批量审批”的需求部门主管一次勾选几十张请假单点一下批量通过。这个场景用 5.17.0 最直接的写法是循环调用 complete但一不注意就会出一个大问题循环里某个任务已被人抢办complete 抛异常整个循环中断前面已完成的审批跟着回滚。因为 ProcessEngine 默认和 Spring 事务绑定在同一条事务链里一处失败全部回滚。我现在的习惯是批量场景下每个任务的 complete 单独提交不共用外部事务。Spring 配置里给批量方法单独标记Transactional(propagation Propagation.REQUIRES_NEW)或者干脆用编程式事务让每个任务独立 commit。伪代码如下ListString taskIds getSelectedTaskIds(); for (String taskId : taskIds) { try { taskService.complete(taskId, buildApproveVars()); } catch (Exception e) { // 记录失败任务继续处理后续任务 failedTaskIds.add(taskId); log.error(任务审批失败: taskId{}, taskId, e); } } if (!failedTaskIds.isEmpty()) { // 收集失败原因给用户明确的失败清单而不是一句“系统错误” throw new BizException(部分任务审批失败已成功 (taskIds.size() - failedTaskIds.size()) 个); }另外推荐一个查询习惯历史流程列表必须带分页不要用list()一把梭。5.17.0 的 HistoryService 分页参数和 7.x 一样是listPage(firstResult, maxResults)配合orderByProcessInstanceStartTime().desc()能把接口响应稳定压在百毫秒内。加上 businessKey 过滤条件能让“按单号查流程”这种高频操作直接命中索引而不是全表扫描。这套组合拳打下来一个基于 activiti-5.17.0 的审批系统还能再健康地跑好几年。老版本不值得盲目换但值得被认真对待——先把批处理事务边界划清楚把历史表归档做起来剩下的交给时间验证。希望这篇笔记能帮到你也祝你的流程引擎少一点半夜告警。本文还有配套的精品资源点击获取