Activiti 5.17.0 老项目流程引擎实战:从表结构到任务流转的完整指南
简介Activiti 5.17.0 是一款面向 Java 开发者的开源工作流与 BPM 平台适用于企业审批流、任务调度、业务自动化等场景。这份官方发布并打包好的 zip 资源总体积约 69.35MB内含可运行的 jar 包、框架源码和使用手册既能让开发者快速部署使用也能通过阅读源码理解引擎的内部运行机制。整套资料围绕 BPMN 2.0 标准展开覆盖流程建模、引擎解析与执行、任务分配与管理、用户与组权限、监听器与事件机制、表单集成、持久化事务以及监控报表等核心模块其中流程定义支持图形化建模任务节点可分配给用户或组并支持条件分支、并发子流程等复杂逻辑事件机制允许开发者在流程启动、任务完成等时机注入自定义业务逻辑解决会签、驳回、多级审批等常见需求。配合表单引擎与持久化存储还能保障数据在流程各节点间正确流转并为系统提供事务一致性与操作安全边界。借助手册还可学习与 Spring、Spring Boot 等主流框架的集成方式用于实际开发中的流程设计、异常处理和性能调优。资源已经获得 202 人浏览/学习适合需要构建 OA 审批、工作流引擎或进行 BPM 技术选型的中高级后端工程师作为入门参考和工程实践资料。1. 为什么还要折腾 Activiti 5.17.0老项目里跑着的流程引擎这几年维护了好几个启动于多年前的 Java 后台服务流程模块全是 activiti版本死死锁在 5.17.0。想升级历史流程数据全部要迁移代价高到没人敢签字不升级网上教程铺天盖地都是 6.x、7.x照着抄一半就发现 API 对不上。于是「吃透老版本」成了接手这类系统的必修课。这篇笔记不聊新特性就讲 5.17.0 从初始化、Spring 集成、流程部署到任务流转的完整链路重点把我在实际项目里踩过的坑摊开说。适合正在维护老系统、需要在 5.17.0 上做二次开发、或者刚接手一堆历史流程数据的开发者。看完你至少能独立部署一个流程并知道出问题时该查哪张表。2. 核心表结构五类表的分工与初始化策略2.1 五类表各管什么先记住哪几张Activiti 5.17.0 建完库会生成以ACT_开头的数十张表按前缀可以分成五组。新手最容易在这里犯迷糊运行时数据、历史数据、流程定义数据混在一起查导致该看ACT_HI_*的时候去看ACT_RU_*自然什么都查不到。前缀用途关键表典型场景ACT_RE_*流程定义、模型等静态资源ACT_RE_PROCDEF、ACT_RE_DEPLOYMENT查流程有几个版本、部署时间ACT_RU_*运行时数据流程执行到一半的状态ACT_RU_TASK、ACT_RU_EXECUTION、ACT_RU_VARIABLE查当前待办任务、当前流程变量ACT_HI_*历史数据流程结束后的归档ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_VARINST做流程审计、统计耗时ACT_ID_*用户、组、关系ACT_ID_USER、ACT_ID_GROUP身份管理很多项目直接不用ACT_GE_*通用数据ACT_GE_PROPERTY、ACT_GE_BYTEARRAY存流程定义的二进制内容平时排障最高频的就是三张ACT_RU_TASK没有记录说明当前没有待办任务要么流程走完了要么卡在别的节点ACT_HI_PROCINST记录了流程实例的起止时间用来判断流程是否走完ACT_RE_PROCDEF每次部署都会产生新版本同一个 key 下 VERSION 字段从 1 开始递增。这里有个常见误用想查「谁处理过这个流程」去查ACT_RU_TASK是看不到的运行时表只保留当前未完成的任务。历史处理记录必须查ACT_HI_TASKINST它保留了每个任务的处理人、开始时间、结束时间。记住了这一点后面第 6 章做流程审计就能直接上手。2.2 引擎初始化databaseSchemaUpdate 的四个取值5.17.0 的引擎初始化核心是ProcessEngineConfiguration。不接 Spring 时默认加载 classpath 下的activiti.cfg.xml接 Spring 后通常改用SpringProcessEngineConfiguration由它来构建ProcessEngine。构建引擎时最关键的参数是databaseSchemaUpdate它决定了表怎么建、什么时候建。bean idprocessEngineConfiguration classorg.activiti.spring.SpringProcessEngineConfiguration property namedataSource refdataSource/ property namedatabaseSchemaUpdate valuetrue/ property nametransactionManager reftransactionManager/ /beandatabaseSchemaUpdate的取值要分清false启动时不做任何 DDL表必须提前存在。生产环境推荐这种避免引擎启动时跟数据库表结构较劲。true启动时对比缺失的表只创建不存在的已有的表不动。本地开发最省心我一般本地用 true接生产前把表结构导出来改成 false。create-drop启动时创建销毁时删除。只适合单元测试用一次后悔一次数据全没了。no-verify跳过表结构校验适合对性能敏感的场景但前提是你非常确定表结构没问题。值得注意的是5.17.0 的建表脚本并不是放在项目里而是打在 activiti-engine 的 jar 包里路径是org/activiti/db/create/按数据库类型分文件比如activiti.mysql.create.engine.sql。日常项目里如果数据库账号权限受限跑不了 DDL手动导这个脚本也是一条路。另外提一句 5.17.0 的底层映射引擎内部走 MyBatis 3.2.8实体映射文件在org/activiti/db/mapping/entity/下。如果哪天需要给表加索引、调整查询去这里能找到对应 SQL 的入口。2.3 依赖坐标与版本匹配JDK 和 Spring 的兼容边界接手老项目遇到的第一道坎是依赖版本。5.17.0 是 JDK 6/7 时代的产物在 JDK 8 下运行基本没问题但如果你把工程升到了 JDK 17 再回来用它反射相关的报错会从启动阶段一路冒出来。dependency groupIdorg.activiti/groupId artifactIdactiviti-engine/artifactId version5.17.0/version /dependency dependency groupIdorg.activiti/groupId artifactIdactiviti-spring/artifactId version5.17.0/version /dependency这里要特别说明的是activiti-spring和activiti-engine的版本必须保持一致这是 5.x 时代的硬规则混用版本容易出现类找不到或者方法签名不一致。配套的 Spring 版本一般落在 3.2.x 到 4.x如果你的工程已经用上 Spring 55.17.0 的很多内部接口跟新版本冲突常见的报错包括序列化相关方法签名变化、ObjectMapper 行为不一致等。数据库驱动也要注意年代差。比如 MySQL 5.6 时代常用的 mysql-connector-java 5.1.x跟 5.17.0 配合得很好如果直接跳到 8.x 驱动连接参数和时区设置都变了常见表现是The server time zone value йʱ is unrecognized这种乱码报错。这种情况不是 activiti 的问题是驱动兼容问题加上serverTimezoneAsia/Shanghai大多能解决。3. Spring 工程接入从依赖配置到部署第一个流程3.1 最小工程pom 依赖和目录结构把 5.17.0 接进 Spring 工程最小依赖就是上一章那三个坐标activiti-engine、activiti-spring、数据库驱动。工程结构我一般这样组织processes/目录放 BPMN 文件config/放引擎配置。dependency groupIdorg.activiti/groupId artifactIdactiviti-engine/artifactId version5.17.0/version /dependency dependency groupIdorg.activiti/groupId artifactIdactiviti-spring/artifactId version5.17.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.39/version /dependency注意点有两个。第一activiti-spring会传递引入spring-beans、spring-tx等模块跟工程里已有的 Spring 版本冲突时需要手动排除传递依赖再显式指定版本。第二如果用到了activiti-rest模块它内部带的 Jackson 版本通常比项目旧JSON 序列化行为两者不一致容易导致接口返回的字段跟预期不同。这类历史版本模块能少引就少引。3.2 ProcessEngineFactoryBean数据源和事务绑定Spring 接入的核心是通过ProcessEngineFactoryBean拿到ProcessEngine实例。它内部干了两件事读取配置构建引擎对象再把 Spring 的事务管理器绑定给引擎让流程操作跟 Service 层事务走同一条路径。bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://127.0.0.1:3306/activiti_db?characterEncodingutf8/ property nameusername valueroot/ property namepassword valueyourpassword/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean bean idprocessEngine classorg.activiti.spring.ProcessEngineFactoryBean property nameprocessEngineConfiguration refprocessEngineConfiguration/ /bean bean idrepositoryService factory-beanprocessEngine factory-methodgetRepositoryService/ bean idruntimeService factory-beanprocessEngine factory-methodgetRuntimeService/ bean idtaskService factory-beanprocessEngine factory-methodgetTaskService/注意这里的factory-methodProcessEngineFactoryBean暴露出来的其实是ProcessEngine对象而repositoryService、runtimeService这些服务是从引擎对象上取出来的。很多初学者在这里犯糊涂以为ProcessEngineFactoryBean直接就是流程引擎其实它是一个工厂 bean取出来的对象才是引擎。数据源上我一般要求连接 URL 必须带characterEncodingutf8不然中文字符串全变问号。事务管理器用 Spring 自带的数据源事务管理器就够不需要额外引入 JTA。这里最关键的一点是配置里的transactionManager必须要和SpringProcessEngineConfiguration里引用的是同一个实例一旦出现两个不同的事务管理器流程操作和数据库操作会各管各的异常时一边回滚一边提交查起问题来十分难受。3.3 部署第一个 BPMNbpmn 文件和 RepositoryService部署流程的第一步是准备一个合法的 BPMN 文件。5.17.0 识别的是 BPMN 2.0 规范命名为.bpmn或.bpmn20.xml均可。一个最简单的请假审批流程长这样?xml version1.0 encodingUTF-8? bpmn2:definitions xmlns:bpmn2http://www.omg.org/spec/BPMN/20100524/MODEL xmlns:activitihttp://activiti.org/bpmn xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance targetNamespacehttp://activiti.org/process bpmn2:process idleaveProcess name请假审批 isExecutabletrue bpmn2:startEvent idstart name开始/ bpmn2:userTask idapplyTask name提交申请 activiti:assignee${applyUser}/ bpmn2:userTask idmanagerApprove name主管审批 activiti:assignee${managerUser}/ bpmn2:endEvent idend name结束/ bpmn2:sequenceFlow idflow1 sourceRefstart targetRefapplyTask/ bpmn2:sequenceFlow idflow2 sourceRefapplyTask targetRefmanagerApprove/ bpmn2:sequenceFlow idflow3 sourceRefmanagerApprove targetRefend/ /bpmn2:process /bpmn2:definitionsprocess的id是流程定义 key启动流程的时候要用它name是显示名称部署后在ACT_RE_PROCDEF里看到的就是这个字段。activiti:assignee指定了任务的处理人这里用了表达式applyUser和managerUser说明处理人是在启动流程时通过变量传入的而不是写死。部署动作通过RepositoryService完成Autowired private RepositoryService repositoryService; public String deployLeaveProcess() { Deployment deployment repositoryService.createDeployment() .name(请假审批流程) .addClasspathResource(processes/leaveProcess.bpmn20.xml) .deploy(); return deployment.getId(); }部署后立刻查一下流程定义确认是否成功ProcessDefinition pd repositoryService.createProcessDefinitionQuery() .processDefinitionKey(leaveProcess) .latestVersion() .singleResult(); System.out.println(流程定义ID: pd.getId()); System.out.println(流程版本号: pd.getVersion());这里先问一个问题部署一次生成一条ACT_RE_PROCDEF记录同一个 key 部署第二次会生成新的 VERSION。如果代码里启动流程时拿的是processDefinitionId并且是从某张业务表里存的 ID 取出来的那它永远指向第一次部署的版本第二次部署的新逻辑就变成摆设。这个坑后面第 5 章会专门展开。还有一点.addClasspathResource是往 classpath 找文件对应的文件要放在src/main/resources/processes/下。很多本地调试跑不通其实就是文件路径没放对报的错很容易误判成引擎问题。4. 流程实例运行变量、任务与监听的完整链路4.1 启动流程实例并传递流程变量流程定义是模板流程实例才是实际跑起来的一条流程。启动实例用RuntimeService.startProcessInstanceByKey第一个参数是 BPMN 文件里的 process id第二个参数是流程变量 Map。Autowired private RuntimeService runtimeService; public String startLeaveProcess(String applyUser, String managerUser) { MapString, Object vars new HashMapString, Object(); vars.put(applyUser, applyUser); vars.put(managerUser, managerUser); vars.put(leaveDays, 3); ProcessInstance pi runtimeService.startProcessInstanceByKey(leaveProcess, vars); return pi.getId(); }启动之后ACT_RU_EXECUTION会插入一条主执行流记录ACT_RU_TASK会出现第一个待办任务也就是applyTask。流程变量有两种存放水平ACT_RU_VARIABLE存运行时的当前变量流程结束后的归档记录在ACT_HI_VARINST。两者通过PROC_INST_ID_关联到同一个流程实例。这里要解释一下变量的存储类型。5.17.0 变量支持最常用的是String、Integer、Long、Boolean、Date和Serializable对象。传对象时会把对象序列化到ACT_GE_BYTEARRAY同时ACT_RU_VARIABLE里记一条指向字节数据的索引。传对象虽然方便但流程变量会跨流程实例长期占用表空间垃圾数据积累起来特别占库非必要不用对象类型能用String或数值就用简单的。4.2 查询并完成用户任务任务流转的核心是查任务、办任务。TaskService是 5.17.0 里用得最频的服务之一。按处理人查待办是日常开发最常见的写法Autowired private TaskService taskService; public void completeApplyTask(String assignee, boolean approved) { ListTask tasks taskService.createTaskQuery() .taskAssignee(assignee) .processDefinitionKey(leaveProcess) .list(); for (Task task : tasks) { MapString, Object taskVars new HashMapString, Object(); taskVars.put(approved, approved); taskService.complete(task.getId(), taskVars); } }taskAssignee是按处理人精确匹配注意它匹配的是ASSIGNEE_字段不是候选人。如果流程节点配置的是activiti:candidateUsers那这里应该用taskCandidateUser来查这两个方法差别很大用错就一条任务都查不到。调用complete后当前任务会被标记完成同时流程按顺序流走到下一个节点。如果下一个节点是用户任务ACT_RU_TASK会插入新的待办记录如果是结束节点整个流程实例结束ACT_RU_EXECUTION和ACT_RU_TASK里的数据都会被清理掉只留下ACT_HI_PROCINST里的一条历史记录。这解释了为什么查运行中的流程要查ACT_RU_*查结束的流程必须查ACT_HI_*。4.3 监听器实战执行监听器和任务监听器5.17.0 的监听器分两类一类是执行监听器ExecutionListener监听流程节点级别的状态变化比如流程开始、节点结束另一类是任务监听器TaskListener监听任务级别的操作比如任务创建、任务完成、指定处理人。建议把执行监听器加在流程元素上bpmn2:process idleaveProcess name请假审批 bpmn2:extensionElements activiti:executionListener eventstart classcom.example.listener.ProcessStartListener/ /bpmn2:extensionElements /bpmn2:process对应的 Java 监听类实现ExecutionListener接口public class ProcessStartListener implements ExecutionListener { Override public void notify(DelegateExecution execution) throws Exception { String processInstanceId execution.getProcessInstanceId(); String businessKey execution.getProcessBusinessKey(); System.out.println(流程实例启动: processInstanceId , 业务键: businessKey); } }参数说明event属性决定监听时机start表示流程节点开始end表示节点结束take表示顺序流被走完时触发。class属性指定的类必须实现了对应监听接口类需要有一个无参构造方法否则引擎反射实例化时会直接报错。实际项目里我见过很多人把监听器当成日志打印器来用这是资源的错配。监听器最适合做的是流程启动后调用外部系统接口同步数据、任务创建后自动发待办通知、流程结束时清理对应的业务状态。要注意监听器代码执行失败会影响整个流程操作所以监听器内部一定要捕获异常不能把业务错误抛到流程引擎这一层。4.4 挂起与激活让流程实例暂停有时候流程需要临时停掉比如节假日暂停审批或者发现问题流程需要冻结处理。5.17.0 的挂起机制分两级挂起流程定义和挂起流程实例。// 挂起整个流程定义新实例不能再启动 repositoryService.suspendProcessDefinitionByKey(leaveProcess); // 挂起单个流程实例当前任务不能再操作 runtimeService.suspendProcessInstanceById(processInstanceId);对应的激活接口是activateProcessDefinitionByKey和activateProcessInstanceById。挂起的流程实例通过TaskService查询任务仍然能查到但complete操作会抛异常。这个特性坑了不少人现象就是任务在列表里点办理却报流程被挂起原因就是没注意流程实例的挂起状态。排查时可以查一个字段ACT_RU_EXECUTION里的SUSPENSION_STATE_值为 1 是激活状态值为 2 是挂起状态。在业务代码里如果要在任务办理前做校验可以先查这个字段给用户一个明确的提示文案而不是让后端抛一堆堆栈。5. 避坑与排错5.17.0 常见的翻车场景在 5.17.0 上踩过的坑数得过来但每一个都曾经让人在深夜里怀疑自己的基本功。下面这几条是按出现频率排序的有同款症状直接照方抓药。5.1 启动报错表不存在或列不存在【现象】Spring 应用启动时 JVM 日志抛出Table ACT_RU_TASK doesnt exist或者数据库管理工具能查到表但启动依旧报列不存在。【原因】databaseSchemaUpdate设置成了false但对应的建表脚本没有执行过还有一种情况是执行了旧版本的 SQL 脚本5.17.0 对部分表结构做了调整。【解决】本地开发先改成true启动一次让引擎自动补表生产环境则去 activiti-engine jar 包org/activiti/db/create/目录下找到对应数据库的脚本按顺序执行engine、identity、history三类脚本。5.2 部署了新流程跑的却还是旧逻辑【现象】BPMN 文件修改后重新部署日志显示部署成功但流程实例跑到旧节点上新加的条件分支不生效。【原因】同一 key 的流程定义会按版本递增代码里启动流程时用的是旧的processDefinitionId而不是最新的。【解决】确认启动方式用的是processDefinitionKey配合.latestVersion()查询最新定义如果你必须存固定 ID部署成功后重新查一次ACT_RE_PROCDEF把新的 ID 更新回业务表。5.3 MySQL 表名大小写不一致导致查询失败【现象】本地 Windows 开发一切正常部署到 Linux 后引擎启动也没报错但某个 SQL 查询报Table act_ru_task doesnt exist。【原因】Activiti 5.17.0 生成的 DDL 里表名是大写ACT_RU_TASKMySQL 在lower_case_table_names0的 Linux 环境下区分大小写而应用里某些查询拼的是小写表名。【解决】统一大小写。最简单的方法是数据库初始化时强制使用大写表名连接 URL 不要加lowerCaseTableNames这类参数如果已经乱了就把小写表重命名成大写再确认所有自定义 SQL 里的表名与库中实际表名完全一致。5.4 流程名称和任务名称中文乱码【现象】流程部署后ACT_RE_PROCDEF.NAME_或任务标题显示成????或者存储后读出来是乱码。【原因】JDBC 连接没有指定characterEncodingutf8数据库表本身字符集不是 utf8两者叠加导致中文在写入时被转成了无法识别的字节。【解决】连接 URL 加characterEncodingutf8数据库建库语句指定DEFAULT CHARACTER SET utf8已经乱的表要去掉脏数据后重建。BPMN 文件本身要确保是 UTF-8 编码保存IDE 默认编码如果不是 UTF-8即使数据库没问题部署时解析 XML 也会出现中文反转义。5.5 事务边界混乱导致数据不一致【现象】流程启动之后业务表数据回滚了但ACT_RU_*表里却留下了流程实例记录或者反过来流程记录回滚了业务表却提交了。【原因】5.17.0 的引擎自身带事务机制接 Spring 后必须让引擎使用外部事务管理器。如果配置里没有正确绑定transactionManager引擎会用自己的事务提交跟业务事务各管各的。【解决】检查SpringProcessEngineConfiguration中的transactionManager是否和 Spring 中的数据源事务管理器是同一个 bean。同时业务方法加上Transactional把流程操作和业务操作放在同一个事务里异常时统一回滚。5.6 日志里看不到引擎执行的 SQL【现象】想调试流程走某一步时执行了什么 SQL但控制台没有任何输出排查效率很低。【原因】5.17.0 的日志默认输出级别没有包含实体查询包的 SQL。【解决】配置 log4j 或 logback把org.activiti.engine.impl.persistence.entity的级别设为DEBUG就能看到 MyBatis 生成的 SQL 和执行参数。这一条在排查任务查询、变量更新这一类逻辑时实测非常管用能让引擎这个「黑匣子」直接打开给你看。6. 进阶用历史表做流程审计与状态自查6.1 没有管理界面时SQL 就是审计工具很多老项目的流程管理后台做得极其简陋连流程走到哪一步都看不到。这时候历史表就是最好的审计工具。我常用这样一条 SQL 拉出一个流程实例的完整轨迹SELECT p.ID_ AS PROC_INST_ID, p.PROC_DEF_ID_, p.START_TIME_, p.END_TIME_, v.NAME_ AS VAR_NAME, v.TEXT_ AS VAR_VALUE FROM ACT_HI_PROCINST p LEFT JOIN ACT_HI_VARINST v ON v.PROC_INST_ID_ p.ID_ WHERE p.PROC_DEF_ID_ LIKE leaveProcess:% ORDER BY p.START_TIME_ DESC;这条查询不需要接口不需要写 Java直接在数据库客户端就能跑。END_TIME_为空说明流程还没走完变量字段可以看到每次申请时传入了什么参数。再看每个任务由谁什么时候处理的SELECT t.NAME_ AS TASK_NAME, t.ASSIGNEE_, t.START_TIME_, t.END_TIME_, t.DELETE_REASON_ FROM ACT_HI_TASKINST t WHERE t.PROC_INST_ID_ 10001 ORDER BY t.START_TIME_;DELETE_REASON_有几个固定值completed表示正常办理完成deleted表示被删除或流程被终止。如果流程实例是异常中断的同一实例下的后续任务可能没有END_TIME_从这里就能快速定位是哪一步断了。6.2 把引擎当工具而不是黑匣子刚接流程引擎那阵子我对 5.17.0 的心态是「能跑就行」出了问题先重启实在不行再看日志。后来吃了几次亏才明白这类开源引擎的表结构、日志开关都是暴露在外的调试接口只是平时没人给你指路。从那以后我每次改完流程都强制走一遍三板斧先查ACT_RE_PROCDEF确认部署的是不是最新版本再启动到目标节点查ACT_RU_TASK核对当前待办最后看ACT_HI_PROCINST和ACT_HI_TASKINST确认整个执行轨迹没有跑偏。一条流程从部署到走通这三个检查点能覆盖九成以上的问题省下的全是排查时间。希望这篇笔记能帮你在 5.17.0 上少走几步弯路。本文还有配套的精品资源点击获取