SpringBoot集成Activiti 7工作流引擎实战:从环境搭建到避坑指南
做企业级应用开发久了你早晚会遇到这样一个需求审批流。从OA里的请假审批、报销审批到电商后台的订单审核、客服工单流转几乎每个系统里都藏着一套“带流程的表”。一开始我都是自己写状态机硬扛后来流程多了、分支多了、审批角色多了那套状态机代码就变成了谁都不想碰的“屎山”。直到我从 SpringBoot 项目里引入 Activiti 7 工作流引擎把流程抽象成 BPMN 定义文件才算真正从增删改查的泥潭里爬出来。这篇文章就围绕 SpringBoot 集成 Activiti 7 这条主线把选型思路、环境搭建、第一个可运行流程、核心 Service API、常见坑位全部过一遍。篇幅不短但我尽量说人话让你看完能直接在自己的项目里跑起来。适合人群是已经会 SpringBoot 基本开发、对“工作流”只有一个模糊概念、想在一个下午之内把 Activiti 7 用起来的后端同学。1. 选型思路SpringBoot 项目中为什么需要工作流引擎1.1 没有工作流引擎时的“土办法”很多小团队接到审批需求第一反应是在业务表上加一个status字段0 待提交、1 待主管审批、2 待经理审批、3 通过、4 驳回。代码写起来也很快if (status 1) { 主管审批逻辑 } else if (...) { 经理审批逻辑 }看起来贼清晰。但业务一复杂就完蛋。比如请假超过 3 天要走经理审批超过 7 天还要走人事备案比如报销金额大于 5000 要总监审批还会签财务和法务再比如审批人驳回后要跳回上上一个节点重新填写。这些规则一旦叠加你写在业务代码里的“状态机”就会膨胀成一大坨难以理解的 if/else 和 switch/case。我自己踩过的坑是改一个审批层级要牵连十几个接口和表结构测试的时候漏一个分支就出事故。工作流引擎解决的核心问题就是把“流程怎么走”从业务代码里剥离出来变成一份独立的、可维护的流程定义文件。流程引擎负责推进节点、记录历史、分发任务业务代码只关心“当前任务是谁的、数据是什么、办完了往下怎么走”。这本质上是一种关注点分离和 SpringBoot 里把 Controller/Service/Mapper 分层是同一个思想。1.2 Activiti 7 的定位与核心竞争力Activiti 是一个用 Java 写的、遵循 BPMN 2.0 规范的开源工作流引擎。它的历史很长早期基于 jBPM后面又衍生出 Flowable 和 Camunda 这些分支。选择 Activiti 7 而不是自己维护状态机是因为它自带几个很实在的能力可视化流程定义流程可以用图形化工具绘制导出 XML 放到项目里非开发人员也能大致看懂审批路径。表结构自动管理引擎启动时会自动创建几十张ACT_开头的表流程定义、流程实例、任务、历史、变量全都有现成模型。流程实例与业务解耦引擎只负责流程推进业务数据存在你自己的表里通过businessKey关联即可。内置丰富的任务策略单人办理、候选人组、会签、或签、驳回、加签这些复杂语义都有 API 支撑。Activiti 7 对应的 SpringBoot 集成方式已经很成熟了引入官方 starter 后基本上只需要配置数据源和少量spring.activiti.*参数就能跑起来。它内部帮你完成了ProcessEngine的创建、核心 Service 的注册、配置文件的加载开箱即用的体验在同类引擎里算是做得不错的。1.3 Activiti、Flowable、Camunda 怎么选如果你在做技术选型一定会纠结这几个名字。说个真实感受Flowable 是从 Activiti 5/6 的代码 fork 出去的社区维护节奏更稳升级路线清晰很多人说它是“业内更稳定的选择”Camunda 也源自 Activiti 早期代码但后来另起炉灶走了轻量、微服务友好的路线运维界面好评很多而 Activiti 7 则是官方分支重新聚焦云原生方向强调 SpringBoot 集成和轻量化嵌入。我的建议比较简单如果团队没有历史包袱单纯想快速在 SpringBoot 里把审批流跑起来Activiti 7 够用如果你要长期维护、需要更保守的版本升级策略Flowable 会更省心如果你们团队已经熟练 BPMN 绘制更看重管理和监控界面Camunda 也值得试。文章后面所有内容都基于 Activiti 7但你如果换成 Flowable 或 Camunda核心概念是大同小异的。2. 环境准备SpringBoot 2.7 Activiti 7 集成2.1 依赖引入与版本选择在 SpringBoot 项目里集成 Activiti 7最稳妥的方式是用官方 starter。我的工程用 SpringBoot 2.7.18Java 8Activiti 版本选择7.1.0.M6。注意这是一个里程碑版本不是最终 release但实际项目中用得非常普遍稳定性在社区已经得到了验证。dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependency如果数据库用 MySQL记得加对应驱动如果用 H2 做本地演示也需要把 H2 依赖带上。这里有个容易被忽略的点Activiti 7 的启动器一般会传递引入spring-boot-starter-jdbc但你自己的项目最好显式确认有 JDBC 相关依赖。另外如果使用 JDK 11 及以上版本很可能需要额外引入 JAXB API 依赖否则启动时会遇到缺失类的问题dependency groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId version2.3.1/version /dependency之所以要加 JAXB是因为 Activiti 内部解析 BPMN XML 时用到了 JAXB而 JDK 11 开始默认不包含 Java EE 模块了。这个问题我最初能被卡十分钟后来直接把依赖加进去才解决。2.2 核心配置项逐个说resources目录下的application.yml是集成第一现场。我一般这样配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/activiti_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root activiti: database-schema-update: true db-history-used: true history-level: full check-process-definitions: true process-definition-location-prefix: classpath*:/processes/ async-executor-activate: false逐个解释这些配置的作用因为这些配置踩坑概率最高database-schema-update是自动建表和更新表结构的开关。有两个常用的值true表示每次启动时检查并更新表结构false表示不会自动变更数据库结构。第一次接入时建议设置为true让引擎帮你建好整套ACT_表部署到生产环境后建议改成false把表结构的变更纳入规范化的数据库变更流程。还有一个值是drop-create每次启动都删表重建这个我只在本地测试用生产环境永远不要碰否则你的历史流程数据直接清零。db-history-used和history-level共同控制历史数据的留存级别。历史级别有none、activity、audit、full四个档位none不记录任何历史activity只记录流程实例和活动节点audit在 activity 基础上增加了变量记录full把更细的执行细节也记下来。绝大多数业务系统用到audit就够了我习惯直接配full方便后续做流程轨迹回放和性能排查代价只是历史表的膨胀速度快一些。check-process-definitions和process-definition-location-prefix决定启动时是否自动扫描并部署流程定义文件。配置为true后Activiti 会在classpath*:/processes/目录下找.bpmn20.xml或.bpmn文件自动完成部署。如果你希望流程定义通过管理后台动态上传可以关掉自动扫描改用手动部署接口。async-executor-activate涉及引擎内部的异步任务执行器。简单需求下把它设为false不需要额外线程池去跑定时任务如果流程里大量使用异步延续、定时边界事件再开启异步执行器也不迟。这个问题属于“基础配置可跑定时任务再研究”的取舍。2.3 数据库准备与初始化在集成前我先在 MySQL 里创建一个空数据库activiti_demo不需要手动建任何表。首次启动应用时Activiti 7 会自动执行自己的建表脚本创建出以ACT_为前缀的几十张表最核心的有ACT_GE_*通用数据表比如属性表ACT_GE_PROPERTY引擎版本信息就存在这里。ACT_RE_*流程定义和流程部署相关的资源表例如ACT_RE_PROCESSDEF存流程定义ACT_RE_DEPLOYMENT存部署包。ACT_RU_*运行时表例如ACT_RU_EXECUTION存流程实例执行信息ACT_RU_TASK存当前待办任务ACT_RU_VARIABLE存流程变量。ACT_HI_*历史表例如ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST。看到这么多表不用慌日常开发涉及的操作基本都通过 Service API 完成很少需要直接 SQL 操作表。但理解表结构有助于排查问题比如待办任务查不到大概率先去ACT_RU_TASK里看一眼当前有没有assignee_对应的记录。2.4 自动装配原理Activiti 是怎么被 SpringBoot 拉起来的很多人引入依赖后会好奇为什么项目里什么都没写ProcessEngine就已经能注入了。这靠的是 SpringBoot 的自动装配机制。activiti-spring-boot-starter里有一个AutoConfiguration它会在应用启动时判断当前 classpath 是否存在 Activiti 相关类如果存在就自动创建一批 Bean。最关键的是ProcessEngine的创建过程自动配置类会拿到 Spring 容器里的DataSource把它交给SpringProcessEngineConfiguration由后者初始化流程引擎。这就意味着如果你的项目里有多个数据源一定要通过Primary明确告诉 Activiti 用哪一个数据源。默认情况下 Activiti 会找容器中唯一的DataSource多数据源时如果不做任何标记很可能注入到错误的数据源上启动时连ACT_GE_PROPERTY都查不到。顺带提一句SpringBoot 2.x 默认使用 CGLIB 代理proxyTargetClass默认开启。这对 Activiti 的影响不大但如果你自己写的 AOP 切面去拦截 Activiti Service 或者流程引擎相关 Bean要注意目标类不能用 final 修饰否则 CGLIB 代理会失败。我见到过有人给ProcessEngine加了自定义切面结果启动报 class 无法继承的错误就是没留神这个细节。3. 第一个可运行的流程请假审批全流程实操3.1 需求案例与 BPMN 设计思路为了不抽象讲概念我用一个非常经典的“请假审批”流程来演示员工提交请假申请如果请假天数小于等于 3 天直接主管审批即可如果大于 3 天除了主管审批还要部门经理审批。审批通过则流程结束审批驳回则直接结束状态为已驳回。流程建模时我会在 IDEA 里用插件绘制流程定义或者用基于 Web 的 BPMN 设计器。绘制完成后导出 XML放到resources/processes/leave.bpmn20.xml。当然手写 XML 也能跑但图形化设计有利于维护。绘制过程中有几个点要特别留意流程的id是引擎识别流程定义的关键后续startProcessInstanceByKey用的就是它建议用有意义的英文名比如leaveProcess。isExecutable属性必须为true否则该流程定义不会被当作可执行流程部署。Activiti 自动扫描时不会校验这个属性是否合法但如果你从建模工具导出的 XML 恰好没有这个标记流程启动时会报“process definition not found”。审批人节点使用UserTask并在activiti:assignee中指定任务办理人。这里可以写死一个用户名也可以不指定运行时通过TaskService设置候选人或者指定 assignee。3.2 编写请假 BPMN 文件的关键片段手工写一个完整的 BPMN 文件很长我这里是核心结构片段process idleaveProcess nameleaveProcess isExecutabletrue startEvent idstartEvent name开始/ userTask idapplyTask name提交请假申请 activiti:assignee${applyUser}/ userTask idmanagerTask name主管审批 activiti:assignee${managerUser}/ exclusiveGateway idgateway1 name天数判断/ userTask iddirectorTask name部门经理审批 activiti:assignee${directorUser}/ endEvent idendEvent name结束/ sequenceFlow idflow1 sourceRefstartEvent targetRefapplyTask/ sequenceFlow idflow2 sourceRefapplyTask targetRefmanagerTask/ sequenceFlow idflow3 sourceRefmanagerTask targetRefgateway1/ sequenceFlow idflow4 sourceRefgateway1 targetRefdirectorTask conditionExpression xsi:typetFormalExpression${days 3}/conditionExpression /sequenceFlow sequenceFlow idflow5 sourceRefgateway1 targetRefendEvent conditionExpression xsi:typetFormalExpression${days 3}/conditionExpression /sequenceFlow sequenceFlow idflow6 sourceRefdirectorTask targetRefendEvent/ /process这里最核心的语法是conditionExpression${days 3}/conditionExpression它对应的就是排他网关上的分支条件。条件表达式基于 UELUnified Expression Languagedays是流程变量在启动流程或者办理任务时通过变量传入。注意条件里的变量名必须和传入的流程变量 key 完全一致否则表达式会拿不到值网关会走默认出口或者报错。3.3 部署流程定义自动扫描与手动部署流程文件放到resources/processes/目录后只要check-process-definitions配置为true应用启动时就会自动部署。每次启动时引擎会比较文件的 MD5 指纹如果同一份流程定义没有变化就不会重复生成新的部署记录。这个机制很方便但你要修改流程内容后重启应用才会生效制定好的流程就不能随便改定义否则已经在跑的历史实例还是按照旧版逻辑走。如果需要动态发布流程可以在后端写手动部署接口。我自制管理后台时常用的代码是Autowired private RepositoryService repositoryService; public void deployProcess(String bpmnFilePath) { InputStream inputStream new FileInputStream(bpmnFilePath); Deployment deployment repositoryService.createDeployment() .name(动态部署- System.currentTimeMillis()) .addInputStream(leave.bpmn20.xml, inputStream) .deploy(); System.out.println(部署成功ID: deployment.getId()); }手动部署的好处是流程文件不用和应用打包在一起随时通过页面或接口上传新版本的流程定义。坏处是你得自己处理版本控制和生效时间。大多数系统在起步阶段用自动扫描就足够了我个人的习惯是把自动扫描关闭或控制好目录只用手动接口来管理生产环境的流程变更。3.4 发起流程实例与任务查询流程部署完成后业务发起端调用RuntimeService启动流程实例。以一个请假单为例Autowired private RuntimeService runtimeService; public void startLeaveProcess(String employeeName, int days) { MapString, Object variables new HashMap(); variables.put(applyUser, employeeName); variables.put(managerUser, zhangsan); variables.put(directorUser, lisi); variables.put(days, days); ProcessInstance processInstance runtimeService.startProcessInstanceByKey(leaveProcess, variables); System.out.println(流程实例ID: processInstance.getId()); }启动后当前任务会落在applyTask这个 userTask 上。员工提交申请这个动作如果建模时activiti:assignee${applyUser}那么applyUser对应的人就能在待办里查到任务。查询待办任务的代码Autowired private TaskService taskService; public ListTask queryTodoTasks(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .orderByTaskCreateTime() .desc() .list(); }这里有个非常常见的坑如果你只查taskAssignee(zhangsan)那候选人任务、组任务都不会被查出来。后面要处理复杂的审批角色还得学习taskCandidateUser、taskCandidateGroup这些查询条件。刚开始实现时不要把所有权限模型都硬塞给 assignee否则后面扩展会很难受。员工填写完请假申请调用TaskService完成这个节点。大多数业务系统会在完成任务时把表单数据保存到业务表里同时把必要的数据塞进流程变量Autowired private TaskService taskService; public void completeApplyTask(String taskId, int days, String reason, boolean duplicate) { MapString, Object variables new HashMap(); variables.put(days, days); variables.put(reason, reason); variables.put(duplicate, duplicate); taskService.complete(taskId, variables); }任务完成后流程沿着flow2走到managerTask主管审批节点。主管进入待办列表看到这张单可以同意或驳回。同意就调用一次complete引擎自动走到网关gateway1根据days变量判断是直接结束还是进入部门经理审批。3.5 完成任务网关条件变量是“活水”注意一个细节网关判断的条件变量来自流程变量而流程变量是可以在流程执行过程中随时更新的。所以如果员工申请时传了days5主管审批时又把days改成了 2网关走向就会发生变化。这是工作流引擎非常灵活的地方也是业务设计时需要警惕的地方流程变量被谁改了、什么时候改的要心中有数。排他网关exclusiveGateway的匹配逻辑是“从上到下找第一个条件为 true 的出口”。如果所有出口条件都不满足又没有设置默认出口default流程执行会抛异常。实际项目中我强烈建议给每个排他网关都配一个default出口这样即使变量缺失或条件写错流程也能正常往下走而不是卡死。如果需求里要“多人会签”比如“部门经理和人事同时审批”只用单一 userTask 加 assignee 是不够的。Activiti 的实现方式是配置activiti:multiInstanceLoopCharacteristics通过sequential决定是串行会签还是并行会签再配合activiti:collection和activiti:elementVariable来遍历审批人集合。这块一开始接触会有点绕但理解了以后会发现它在“多人审批”“比例通过”这类场景下特别能打。要注意的是会签任务完成时引擎会动态生成多个任务实例ACT_RU_TASK中的IS_COUNT_ENABLED_和TASK_DEF_KEY_就是用来标识这类任务实例的。4. 流程推进的核心 Service API 详解4.1 RepositoryService流程定义的生命周期管理RepositoryService在七个核心 Service 里分管“静态资源”。流程部署、流程定义查询、流程定义挂起/激活、删除部署记录都走这个 Service。常用操作我整理成一份笔记部署repositoryService.createDeployment().addBytes(...).addInputStream(...).deploy()。查询流程定义repositoryService.createProcessDefinitionQuery().processDefinitionKey(leaveProcess).latestVersion().singleResult()。注意一个 key 会对应多个版本latestVersion()只拿最新版。挂起/激活repositoryService.suspendProcessDefinitionById(definitionId)/activateProcessDefinitionById(...)。挂起后这个流程定义不能再发起新流程实例但已存在的实例不受影响。删除部署repositoryService.deleteDeployment(deploymentId)。这里有个坑如果部署下已经有流程实例或者历史记录普通的 delete 会抛异常需要用deleteDeployment(id, true)执行级联删除。级联删除很暴力会把这个部署下的所有历史流程记录全部清掉生产环境慎用。在线演示环境里我最常翻车的就是“部署成功后却启动不了流程”原因基本是版本没查对或者流程定义处于挂起状态。排查时可以写一个简单的查询接口把流程定义列表拉出来看一眼SUSPENSION_STATE_为 2 就是挂起状态。4.2 RuntimeService流程实例的启动与状态控制RuntimeService管“运行中的动态数据”。流程启动、信号事件、执行实例查询、流程变量增删改查都在这里。和RepositoryService最大的区别是前者管定义后者管实例。启动流程有三种常见方式startProcessInstanceById(processDefinitionId)按定义 ID 启动每次都要先查出来定义 ID。startProcessInstanceByKey(processDefinitionKey)按定义 key 启动默认启动最新版本业务系统最常用。startProcessInstanceByKeyAndBusinessKey(key, businessKey)额外把业务主键关联进去这个业务主键可以是请假单 ID、订单 ID、工单 ID。有了 businessKey 之后HistoryService查询历史流程时可以像查业务表一样方便。ProcessInstance processInstance runtimeService.startProcessInstanceByKeyAndBusinessKey( leaveProcess, LEAVE-2025-0001, variables );流程变量这块RuntimeService提供setVariable(executionId, key, value)、setVariables(executionId, map)、getVariable(executionId, key)。同一个流程实例下不同执行节点上的变量作用域不同涉及并行分支时尤其要小心我在实际项目里因为变量作用域问题吃过亏后面会专门提一下。4.3 TaskService待办任务的查询与办理TaskService是实际业务代码里调用频率最高的接口。只要登录用户要对审批单做操作底层一定跑的是 TaskService。核心能力可以分三类任务查询createTaskQuery()可以组合出大量条件。除了taskAssignee还有taskCandidateUser候选人中包含当前用户、taskCandidateGroup候选组中包含当前组、processDefinitionKey、taskCreatedAfter、taskCreatedBefore等。查询条件越精准SQL 定位越高效也越不容易查错数据。任务办理complete是完成任务的主入口有complete(taskId)和complete(taskId, variables)两种重载。每次 complete 都是引擎状态机的关键转折点流程会立即推进到下一个节点。如果下一个节点依赖缺失变量这里就可能抛异常。任务分配setAssignee(taskId, userId)把任务指派给某个具体用户addCandidateUser把用户加入候选人名单claim表示认领任务认领后 assignee 变更为当前人unclaim撤销认领把 assignee 清空。市面上大部分 OA 系统的“取回”“转办”“委派”功能底层基本都是这些方法组合出来的。我一开始接工作流的时候特别喜欢在业务表里搞一堆抄送记录后来发现直接复用 TaskService 的候选人机制就特别舒服抄送给谁就把谁加到 candidateUser 里对方按taskCandidateUser查就能看到。4.4 HistoryService历史记录与流程追溯如果说前三类是“现在时”HistoryService就是“过去时”。它的核心价值有两个一是做“已办列表”和“流程轨迹回放”二是做流程分析。已办列表要从历史任务表里查Autowired private HistoryService historyService; public ListHistoricTaskInstance queryDoneTasks(String assignee) { return historyService.createHistoricTaskInstanceQuery() .taskAssignee(assignee) .finished() .orderByHistoricTaskInstanceEndTime() .desc() .list(); }流程轨迹回放则是把流程实例经过的所有活动节点按时间捞出来ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list();有了这个列表前端就能画出类似“已走路径”的时序图。很多项目还要在流程图高亮显示当前节点用repositoryService.getProcessDiagram配合historyService.createHistoricActivityInstanceQuery().finished()判断哪些节点已经完成这算是一个标准的扩展点。注意流程图片生成时如果节点名称涉及中文在 Linux 服务器上很容易出现豆腐块乱码需要设置流程图字体我之前在项目里吃过这个亏。4.5 其他 Service 的补充说明IdentityService管理用户、组以及用户和组之间的关系。在实际企业系统中用户体系通常在业务系统里已经建好很多人干脆不通过 IdentityService 管理用户而是直接使用用户 ID 字符串作为 assignee。这样集成成本低但流程引擎内部不会感知这些用户是否真实存在。ManagementService主要用于引擎运维比如执行数据库命令、查询引擎属性。这类操作很少出现在业务代码里。FormService管理流程表单数据。如果项目前后端分离、表单完全由前端控制FormService 用得不多如果依赖 Activiti 自身的表单引擎则需要在流程定义中配置 formKey。DynamicBpmnService7.x 提供可以动态修改流程定义中的节点信息比如动态改审批人、修改节点名称在个性化需求重的系统里是宝贵的工具箱。5. 常见问题与避坑指南5.1 启动失败DataSource 循环依赖与自动配置覆盖我见过最多的集成问题都出在多数据源场景。用户引入了 Activiti 依赖后项目里本来已经有一个主数据源和几个业务数据源没有做任何标记。启动过程中 Activiti 试图自动注入一个DataSource发现容器里有多个候选 Bean直接报NoUniqueBeanDefinitionException。解决方式也很简单在主数据源类上加上Primary让 Activiti 明确选择它。Bean Primary ConfigurationProperties(prefix spring.datasource) public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); }另一个相关问题SpringBoot 2.6 之后默认禁止循环依赖而某些版本的 Activiti 自动配置和用户自定义配置之间存在循环引用导致启动报The dependencies of some of the beans in the application context form a cycle。遇到这种提示先排查是否真的是自己的 Bean 设计有问题。如果确定没问题只是第三方配置需要可以在配置里设置spring.main.allow-circular-referencestrue但我不太建议用这个开关来掩盖问题根治还是要看清依赖图。5.2 流程图片显示乱码与流程文件编码部署到服务器后用户查看流程图发现所有中文节点都显示成方框这是典型的 Linux 服务器字体缺失或 Java 图形环境字体不正确引起的。基本解法是在生成流程图的时候显式指定字体名称同时确保服务器上有对应的中文字体比如宋体或WenQuanYi。ProcessDiagramGenerator generator processEngine.getProcessEngineConfiguration() .getProcessDiagramGenerator(); InputStream diagram generator.generateDiagram( bpmnModel, png, activityIds, flowIds, 宋体, 宋体, 宋体 );generateDiagram里传的三个字体参数分别对应活动节点字体、连接线字体、标注字体。如果你本地用 Windows 开发时不指定字体也能正常显示一上 CentOS 服务就乱码就是没传字体的锅。流程文件的编码也容易踩坑processes目录下的 BPMN 文件如果包含中文注释必须保证文件是 UTF-8 编码否则解析阶段可能出现乱码导致 XML 解析失败启动时直接报错。IDEA 里一般默认 UTF-8但从同事电脑拷贝过来的文件有可能被改过编码遇到异常先看文件实际编码。5.3 流程变量作用域本地变量还是全局变量在并行网关场景中这个问题几乎一定会出现。比如流程在并行分支中分别执行 A 节点和 B 节点A 分支设置了一个变量B 分支却读不到查ACT_RU_VARIABLE发现变量在某个 execution 上不在根执行上。Activiti 7 支持局部变量Local Variable和全局变量Global Variable。setVariable默认是设置当前执行实例的变量如果这个执行实例不是根执行实例那其他分支无法观察到setVariableLocal则显式只设置在本地执行实例上。反过来往根执行实例设置变量所有分支都能看到。一个保险做法是流程变量统一通过runtimeService.setVariable(processInstanceId, key, value)设置通过流程实例 ID 去设置会落在根执行实例上这样全局都能读到。在网关条件判断时所有分支读取到的都是这个根实例范围内的变量不容易出现变量丢失问题。5.4 历史级别太低导致无法排查问题在一个快速迭代的项目里我想节省数据库空间把history-level配置成了none。结果后来一个流程实例走到网关后丢失问题怎么也查不到因为历史表全是空的。想回滚一个错误操作的流程实例也没有任何记录可供参考。从那以后我再也不用none。建议最低用activity但业务系统至少用audit。因为audit会保留流程变量历史当你需要复盘某个审批单当时传入的参数是什么时直接查历史变量表就能还原现场。生产环境如果担心数据膨胀可以做定期归档和清理不要以牺牲可追溯性为代价。5.5 MySQL 8 驱动与建表大小写敏感性当前很多项目已经切到 MySQL 8如果使用com.mysql.jdbc.Driver这种老驱动启动时大概率报Loading class com.mysql.jdbc.Driver is deprecated或者连不上。要把驱动配置改成com.mysql.cj.jdbc.Driver同时在 JDBC URL 中加上serverTimezoneAsia/Shanghai不然时区问题会引发一系列时间写入异常。另外Linux 上的 MySQL 默认对库名和表名区分大小写Activiti 的建表脚本默认使用小写表名如果你在 Windows 本地建表时使用了大写、然后同步到 Linux 环境可能在后续操作时报“表不存在”。建议数据库配置里设置lower_case_table_names1并规范表名都是小写。5.6 流程启动时提示找不到流程定义这是新手集成最容易困惑的地方。明明流程文件放在processes目录下也确认了自动扫描配置但调用startProcessInstanceByKey时报“no process definition found with key”。排查重点是自动部署到底有没有执行成功。可以先打印repositoryService.createProcessDefinitionQuery().list()看返回结果里有没有对应 key。如果列表里有很可能是调用时 key 写错了注意大小写完全一致如果列表为空大概率是 BPMN 文件解析失败导致自动部署被跳过此时要注意查看应用启动日志中是否有Deployment相关的异常信息也要检查流程文件后缀名是否被识别。有时候项目里多个模块分别放了自己的 BPMN 文件资源路径不合适导致文件没被扫描到也要确认process-definition-location-prefix的实际路径和文件位置匹配。最后再分享一个实际项目中使用的小技巧在我接手过一个老系统改造后意识到一个特别重要的工作习惯流程定义文件一定要纳入版本管理并且每调整一次 BPMN就要更新一次流程版本生产环境不要轻易覆盖旧版本。Activiti 每次部署都会生成新的版本号旧实例继续走旧逻辑新单走新逻辑这种并行兼容能力在业务迁移时极其好用。再加上我习惯用businessKey把业务主键贯穿始终任何时刻想排查一张单走到哪个环节只需要一个查询接口就能把运行实例、历史活动、当前待办串联起来。指望一个下午就把工作流引擎带来的所有能力吸收掉不现实但搭起来一个可用的 SpringBoot Activiti 7 环境、跑通一个最简单的审批流程完全是可以的。后面再慢慢啃 BPMN 的各种事件、子流程、会签和审批策略遇到问题回来再翻这篇文章里的避坑点应该能帮你少走一大段弯路。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →