Activiti 5.17.0工作流引擎实战:BPMN部署、任务流转与避坑指南
简介Activiti 5.17.0 是开源工作流引擎 Activiti 的官方发布包面向需要在 Java 企业级应用中实现流程自动化的开发者提供从流程建模到运行监控的完整解决方案。压缩包为 zip 格式约 69.35MB内含可运行 jar 包、引擎源码与官方参考手册便于从代码层面理解机制并结合文档快速上手。已有 202 人学习适合具备 Java 基础、希望深入 BPM 领域的读者。资源围绕流程定义、引擎执行、任务分配、事件监听等核心模块展开可掌握 BPMN 2.0 流程建模、流程实例的启动与流转控制、与 Spring 框架的集成方式以及基于角色的访问控制等关键实践。同时通过持久化与事务管理、监控报表等进阶内容可进一步理解企业级流程系统的设计思路为构建高效灵活的业务自动化解决方案打下扎实基础。1. Activiti 5.17.0工作流引擎的老兵凭什么还能打Activiti 这个开源工作流引擎在 BPM 领域摸爬滚打多年5.17.0 虽然被很多后来者称为古董版本但直到今天大量企业级审批系统、工单系统、OA 流程模块还在用它做底层支撑。这里说的还能打不是情怀而是它把 BPMN 2.0 规范落地得足够扎实流程部署、任务流转、会签或签、流程变量传递、历史数据追溯这些高频功能用 Java API 就能直接驱动不需要额外引入重型中间件。更关键的是5.17.0 这个版本对 Spring 生态的适配非常顺滑Tomcat 7/8、JDK 6/7/8 都能跑得动很多老系统升级改造时反而愿意选它而不是新版本。适合谁适合那些接手遗留系统、需要在既有 Java 工程里嵌入流程能力、又不想引入微服务级工作流平台的从业者。我下面会把环境搭建、核心 API、部署流程定义到踩坑排障完整过一遍每一步都可以照着复现。2. 搭建运行环境从依赖到数据库的完整落地2.1 引擎核心依赖与版本选型先明确一件事Activiti 5.17.0 不是 Spring Boot 时代的产物它当时的主流用法是 Spring 4.x MyBatis 3.x 那套 SSE 架构。这里涉及的依赖冲突是第一个必须正视的坑。我建议直接用 Maven 管理依赖避免手动拷 jar 包时漏掉传递依赖。dependencyManagement dependencies 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 groupIdorg.activiti/groupId artifactIdactiviti-bpmn-converter/artifactId version5.17.0/version /dependency /dependencies /dependencyManagement这段配置里activiti-engine 是引擎核心包含流程定义解析、执行、任务管理等全部逻辑activiti-spring 是 Spring 集成模块负责把引擎声明为 Spring Bean 并接入事务管理它依赖了 Spring 4.x 系列的 context、tx 等组件activiti-bpmn-converter 负责 BPMN XML 文件与引擎内部模型的相互转换。注意一点5.17.0 的 BPMN 转换器默认只支持到 BPMN 2.0 的部分子集像 CMMN、DMN 这些后续版本扩展它完全不管。2.2 数据库初始化务别用自动建表很多人在第一次接触 Activiti 时会习惯性把它当成 ORM指望着字段映射到数据库表结构。实际上 Activiti 的表结构是完全独立的元数据模型包含 ACT_RE_流程定义与流程模型、ACT_RU_运行期流程实例、任务、变量、ACT_HI_历史数据、ACT_ID_用户与组。生产环境必须手动初始化表结构推荐用引擎自带的 SQL 脚本而不是让引擎启动时自动建表。-- 以 MySQL 5.6 为例执行引擎自带脚本 mysql -u root -p activiti activiti.mysql.create.engine.sql mysql -u root -p activiti activiti.mysql.create.history.sql mysql -u root -p activiti activiti.mysql.create.identity.sql三条脚本分别对应引擎表、历史表、身份模块表。执行顺序不能颠倒因为历史表的外键关联到引擎表。建表后在 activiti.cfg.xml 或 Spring 的 processEngineConfiguration 里设置 databaseSchemaUpdatefalse这样引擎启动时就不会再次创建或变更表。这个参数有四个可选值true 表示每次启动检查并更新表结构false 表示只校验表缺失直接报错create-drop 用于单元测试drop-create 不建议用于任何环境。我一般用的组合是开发环境 databaseSchemaUpdatetrue 图省事测试和生产环境强制 false 并配合数据库迁移工具管理变更。2.3 引擎配置全参数与调试开关Activiti 5.17.0 的引擎配置类 ProcessEngineConfiguration 支持大量参数调节比较关键的是这几个jdbcUrl 与 jdbcDriver 决定底层数据源连接方式jobExecutorActivate 是否激活定时任务执行器处理延迟任务时必须开history 级别参数决定历史数据保留粒度可选 none、activity、audit、full其中 audit 是默认值记录流程实例与活动实例full 追加变量变更明细生产环境如果做流程审计建议用 full代价是历史表数据量会明显增长。bean idprocessEngineConfiguration classorg.activiti.spring.SpringProcessEngineConfiguration property namedataSource refdataSource/ property namedatabaseSchemaUpdate valuefalse/ property namejobExecutorActivate valuetrue/ property namehistory valuefull/ property nameactivityFontName value宋体/ property namelabelFontName value宋体/ /bean这里 activityFontName 和 labelFontName 是专门给流程图的节点名称和标签设置的字体。如果不去设置它使用 Tomcat 等运行环境生成流程图片时中文字符大概率会变成方块乱码这是一个在流程审批界面里很难排查的问题。上段配置里还可以加 processDefinitionCacheLimit 参数比如设置为 100限制缓存解析后的流程定义数量避免某条流程定义版本极多时把 JVM 堆撑爆。3. 部署与驱动流程实例核心 API 的实战操作3.1 RepositoryService 部署 BPMN 文件流程定义是流程流转的图纸图纸不部署引擎就不会执行。使用 RepositoryService 完成部署是最基础的环节这里必须做到完全可控部署前先检查 BPMN 文件里的 targetNamespace 与 process id 是否重复否则同 id 的多次部署会生成多个版本。// 获取 RepositoryService RepositoryService repositoryService processEngine.getRepositoryService(); // 部署流程定义 DeploymentBuilder builder repositoryService.createDeployment() .addClasspathResource(processes/leave.bpmn) .name(请假流程-版本1) .category(hr); String deploymentId builder.deploy().getId();部署成功后引擎会解析 bpmn 文件并生成部署记录与流程定义记录此时流程定义还未与任何流程实例绑定。参数上category 是可选的建议把业务域放进去addClasspathResource 路径需要参考 classpath 根目录BPMN 文件如果不是 XML 后缀也可以使用 addInputStream 配合 FileInputStream 读取磁盘路径。接下来常用的一条操作是查询部署记录验证结果repositoryService.createDeploymentQuery().deploymentId(deploymentId).singleResult()如果返回 null去日志里查 BPMN 解析异常常见的低级错误是文件编码不是 UTF-8 导致中文乱码导致 id 解析失败。3.2 RuntimeService 启动流程实例并关联业务 Key部署完成后启动流程实例就是用 RuntimeService 完成。这里有个容易搞反的概念流程定义是模板流程实例是把模板跑起来的一次实际流转。业务系统通常会把业务单据的 ID 绑定到流程实例上的 businessKey这是后续查询流程与业务关联数据的核心依据。RuntimeService runtimeService processEngine.getRuntimeService(); // 启动流程实例并携带业务唯一标识 ProcessInstance processInstance runtimeService .startProcessInstanceByKey(leaveProcess, BIZ-LEAVE-2024001); // 获取流程实例 ID 与业务 Key String processInstanceId processInstance.getId(); String businessKey processInstance.getBusinessKey();startProcessInstanceByKey 接收的第一个参数是 BPMN 文件中 process id也就是流程定义 key。这里如果传错 id引擎会抛出 ActivitiObjectNotFoundException提示类似no processes deployed with key。businessKey 并非必填但强烈建议填原因很现实流程实例 ID 是引擎内部分配的自增数字串业务系统里保存这个字段没有业务意义你肯定是想用单据号反查流程。另外可以给启动方法加上变量集合用于初始化流程变量这个在下一节展开。3.3 TaskService 完成用户任务与委派操作流程实例启动后遇到 UserTask 节点会生成用户任务并分配一个 assignee办理人此时轮转操作没有真正触发等待办理人操作后由 TaskService 驱动。核心操作就是完成任务与动态更改处理人。TaskService taskService processEngine.getTaskService(); // 查询指定用户的任务列表带排序 ListTask tasks taskService.createTaskQuery() .taskAssignee(zhangsan) .orderByTaskCreateTime().desc() .list(); // 完成任务并带出下个节点需要的变量 taskService.setVariable(taskId, managerApproved, true); taskService.complete(taskId);这里的关键点在于complete 之前业务流程可能正处于等待状态引擎不会自动往流程变量里放值。setVariable 可以设置到一个范围默认是 task-local 还是全局变量取决于你调用的方法setVariable 是流程实例级变量taskService.setVariableLocal 才是任务本地变量。当流程走到下个节点时如果这个节点的条件表达式引用 managerApproved而它不在流程实例级变量里表达式会判断为 null 并直接按 false 处理。委派操作使用 taskService.delegateTask(taskId, lisi) 时实际上是把 assignee 变为 DelegationStatePENDING 状态等 lisi 处理完成后再调用 resolveTask 才真正回传给原处理人很多新手会把 delegateTask 和 setAssignee 混为一谈这是流程参与人配置混乱的常见来源。4. 流程模型与监听器从流程图到运行时扩展4.1 BPMN 文件设计要点与 Eisenhower 条件工作流引擎只有在接收到符合规范的 BPMN 2.0 模型时才可用手写 XML 可以但推荐用 Eclipse 的 BPMN 插件或在线建模工具可视化绘制然后导出 XML。设计阶段有几个字段要特别注意processName 要服务于流程分类与搜索UserTask 的 assignee 或 candidateUsers/candidateGroups 决定任务分发方式排他网关用 conditionExpression 分支流转条件控制走向。process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstartEvent name开始/ sequenceFlow idflow1 sourceRefstartEvent targetRefuserTask / userTask iduserTask name填写请假单 activiti:assignee${applyUser}/ exclusiveGateway idgateway1/ sequenceFlow idflow2 sourceRefuserTask targetRefgateway1 / sequenceFlow idflow3 sourceRefgateway1 targetRefleaderTask name小于3天 conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow sequenceFlow idflow4 sourceRefgateway1 targetRefmanagerTask name大于3天 conditionExpression xsi:typetFormalExpression ![CDATA[${days 3}]] /conditionExpression /sequenceFlow /process需要注意两点。第一BPMN 2.0 XML 中布尔表达式必须放在 CDATA 区块内否则 XML 解析器在实际存储时会对特殊符号进行转义表达式等于废掉运行时排他网关会报表达式无值错误。第二assignee 支持 UEL 表达式这里 applyUser 是在启动流程实例时通过流程变量注入的动态值可以用 runtimeService.startProcessInstanceByKey(key, businessKey, variables) 传入 MapString,Object。凡是可以写成静态字符串的场景我建议全部都改成表达式免得后续流程人员调整后还要改 XML 再重部署表达式允许运行时注入参与人。4.2 ExecutionListener 与 TaskListener 的应用场景Activiti 提供多种监听器用于在流程生命周期与任务生命周期插入自定义逻辑这是流程引擎能够与业务系统解耦的关键点。ExecudtionListener 挂在流程节点的 start 和 end 事件上TaskListener 挂在 UserTask 的 create、assignment、complete、delete 事件上。最常用的是把创建任务时动态指定审批人以及完成节点时触发外部 RPC 通知等逻辑放在监听器里。public class ManagerTaskListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { // 任务创建时从流程变量中读取主管信息并设置为办理人 String manager (String) delegateTask.getVariable(managerId); if (manager ! null) { delegateTask.setAssignee(manager); } else { // 变量缺失时回退到默认审批组防止出现无主任务 delegateTask.addCandidateGroup(approval-default); } } }TaskListener 注册后在 BPMN XML 的 UserTask 节点中添加activiti:taskListener eventcreate classcom.example.ManagerTaskListener /。这里有个血泪经验监听器的 class 属性对应的是全限定类名引擎通过反射创建实例它不会走 Spring 容器管理的 Bean因此类内部如果要调用其他 Spring Bean不能依赖 Autowired 自动注入最简单的做法是把需要调用的 Bean 静态化或改由监听器实现 ApplicationContextAware 接口自己取容器。这个问题坑过不少人——流程能跑通但监听器里拿到的 Service 全是 null其实就是容器上下文没进入反射创建的对象。另一个高发坑是 TaskListener 中调用 taskService.complete 造成递归导致监听器事件被重复触发。因为在 complete 事件里又调 complete等于在同一个流程节点上无限循环卡死。正确姿势是只修改办理人或变量不驱动流程跳转跳转交给流程引擎原生顺序流完成。4.3 流程变量作用域与序列化方式流程变量是流程实例级的但操作上要区分全局变量与本地变量容易踩坑的场景是并行网关后面的多个分支。并行网关会产生多实例执行路径每条执行路径都有一份本地变量副本如果某条路径更新了本地变量其他路径看不见一旦流程合并后选择哪份变量取决于引擎的合并顺序结果不可预测。MapString, Object vars new HashMapString, Object(); vars.put(days, 2); vars.put(applyUser, zhangsan); vars.put(managerId, manager01); ProcessInstance pi runtimeService .startProcessInstanceByKey(leaveProcess, BIZ-2024-001, vars);变量类型支持 Java 基础类型与可序列化对象默认使用 JAVA 序列化。注意 JAVA 序列化机制要求变量类实现 java.io.Serializable 接口反序列化时类结构变了就会抛 InvalidClassException。生产环境我建议改用 JAXB 或 JSON 格式存储流程变量引擎配置中设置 jpaPersistenceProvider 等等比较麻烦更轻量的做法是传递 Map 或字符串类型的 DTO复杂业务对象放到业务表里流程变量只存业务主键减少引擎与业务的数据耦合。变量体积过大、单条变量大小超过数据库单列长度时MySQL 中该行数据会写入异常这也是流程实例突然启动失败的排查方向之一。5. 避坑指南Activiti 5.17 使用中的常见问题与排查5.1 流程图片中文乱码的根源与处理现象调用 ProcessDiagramGenerator 生成流程实例的高亮流程图保存为 PNG 后节点名称的中文字符全变成空心的方块。原因引擎为了绘制图片使用 AWT 类库在服务器环境里没有安装中文字体或者 JVM 的字体配置里找不到可用字体集。解决引擎配置里用 activityFontName 指定了某个字体但服务器实际没有这个字体是不行的。操作层面的做法是在 catalina.sh 或启动脚本上添加参数 -Djava.awt.headlesstrue同时确认服务器安装了 fontconfig 和一款中文字体例如在 CentOS 上执行 yum install fontconfig 后查看 /usr/share/fonts 目录。还有一种快刀斩乱麻的做法不直接依赖引擎绘制图片而是用 activiti 生成的节点坐标数据在前端绘制 SVG以完全绕开 AWT 环境缺点是前端要自己实现连线与高亮逻辑。5.2 流程部署成功但启动抛对象找不到异常现象使用 repositoryService.createDeployment().addClasspathResource(processes/leave.bpmn).deploy() 后调用 processEngine.getRuntimeService().startProcessInstanceByKey(leaveProcess)抛 ActivitiObjectNotFoundException 提示 no processes deployed with key。原因部署操作可能执行失败或者根本没有执行而 fail 时如果部署代码没有捕获异常流程引擎数据库里没有对应 key 的定义。另外要区分流程定义 key 与流程部署 keystartProcessInstanceByKey 接收的是 process id 属性而不是部署名称两者容易混淆。解决先查 ACT_RE_PROCDEF 表确认 key然后检查代码中 ProcessEngine 是否共用同一个数据库实例开发环境最常见的是内存数据库 H2 与业务 MySQL 之间串了部署写进 H2启动查询走了 MySQL两边数据自然对不上。5.3 排他网关全部分支不满足流程实例卡死现象流程到达某个 ExclusiveGateway 后不往任何方向流转实例状态一直停留任务表查不到任何待办。原因排他网关默认行为是在所有外出顺序流里选择第一个条件表达式为 true 的如果全部条件都不成立又没有设置默认出口 flow就会直接卡在网关上同时多数版本也不会立即抛异常很容易拖着不处理。解决第一种方案在 BPMN 里给网关加 activiti:defaultFlow 指向一个兜底节点第二种方案检查条件表达式中的变量值尤其是数字类型取值流程变量里存的 days 可能是字符串“2”表达式 ${days 2} 在 UEL 中使用强制拆箱可能因类型不一致而计算结果为 false建议表达式里写 ${days 2} 前先确认变量类型或统一转换为 Integer。这个问题的排查时间往往远大于部署时间优先排查变量类型。5.4 历史表数据增长太快查询性能劣化现象上线几个月后ACT_HI_ACTINST 与 ACT_HI_VARINST 数据量超过千万级任何查询操作都明显变慢管理页面的流程追踪功能直接超时。原因history 级别设置为 full且流程中有大量重复节点与变量每次变量变更都会在 ACT_HI_* 表留下多行记录加上没有归档机制表膨胀直接影响关联查询。解决把历史配置调整到 audit 级别全量变量变更不需要就降级对历史表定期迁移把三个月前的数据搬进归档库清理已结束流程实例Activiti 原生提供 historyService.deleteHistoricProcessInstance(processInstanceId) 方法。设计层面减少循环和并行节点里的变量变更是最佳做法不要在任务监听器里频繁 setVariable。6. 进阶技巧把流程图从黑匣子变成可视化工具流程引擎跑起来后业务方最大的诉求往往是流程走到哪一步了Activiti 提供生成流程实例当前高亮图片的能力但直接输出 PNG 的服务通常不适合 web 端展示。我习惯的通用做法是把引擎生成的节点坐标和 id 转成 JSON 串前端用开源的流程渲染库去画。核心是拿到流程定义的活动节点坐标信息。// 从 BPMN 模型中提取节点坐标与类型 BpmnModel bpmnModel repositoryService.getBpmnModel(processDefinitionId); ListGraphicInfo graphicInfoList bpmnModel.getGraphicInfoList(); for (GraphicInfo gi : graphicInfoList) { System.out.println(x gi.getX() , y gi.getY() , width gi.getWidth() , height gi.getHeight()); }拿到坐标后与当前活动的节点列表比对前端就能实现节点高亮、连线描红等交互效果。这套方案的好处是不依赖服务器端字体彻底避免了乱码问题代价是 BPMN 模型中的坐标是基于设计器生成的绝对坐标如果 BPMN 文件是从别人手里拿到的经过改动坐标与实际显示可能对不上需重新通过建模工具保存一次。从那以后我每次做流程对接都会先看一眼图形坐标是否与预期一致确认后端生成的 JSON 能够被前端准确渲染避免把流程页面做成一堆错位签字框。流程引擎本质上是把流转逻辑和业务逻辑做了分离把引擎的 API 边界摸熟、把变量作用域摸清剩下的就是按照业务规则去填充节点——最忌惮的是用代码硬编码流程路由把工作流引擎当成一个大号 if/else那样反而白白增加系统复杂度。希望这篇文章能帮你跳过那些我踩过的坑把 Activiti 5.17.0 用得更从容一点。本文还有配套的精品资源点击获取