做后端时间久了,总会遇到一个绕不开的需求:审批。不管是请假、报销、合同会签还是订单审核,本质上都是一条带状态的流转链路。早些年大家喜欢自己在数据库里硬写状态字段,用if-else把流程状态机搓出来,前两个月还好,一旦节点变多、分支一多,代码就变成了大型翻车现场。后来我把目光转到工作流引擎上,试了Activiti 7搭配SpringBoot,才发现这才是做审批系统的正路。
这篇内容我不想把它写成官方文档翻译,而是以一个实际做过项目的角度,把SpringBoot 集成 Activiti 7 工作流引擎这件事讲透:从版本选型、环境搭建、BPMN流程设计、核心API使用,到联调阶段踩过的坑和优化经验,全部整理出来。无论你是第一次接触工作流,还是从Activiti 5/6 往7迁移的老手,这篇都应该能给你省下不少时间。
1. 项目概述与方案选型
1.1 为什么是Activiti 7而不是Flowable、Camunda
先说结论:工作流引擎这个领域,能打的就那几家,Activiti、Flowable、Camunda,它们同根同源,都是从JBPM那棵树上长出来的。Flowable是Activiti 5/6时代分叉出去的,Camunda也类似,但商业化程度更高,生态更完整。那我为什么在SpringBoot项目里选了Activiti 7?
核心原因是轻量。Activiti 7 在设计上做了非常大的模块化重构,引擎核心不再和Spring强绑定,你可以把它的引擎理解成一个独立的Java库,SpringBoot只是负责把它“接进来”。相比Camunda动辄引入一堆Web应用和Cockpit控制台,Activiti 7 更像一个纯粹的流程引擎,特别适合那种只需要BPMN流程解析、任务分发、流程实例管理的后端项目。
另一个原因是国内社区的资料量。虽然Activiti官方文档更新有点随缘,但国内对Activiti的讨论、踩坑记录、二次开发案例确实很多,遇到奇葩问题基本都能搜到答案。Flowable文档更规范,但版本更新太快,社区问答沉淀反而少一些。团队如果有历史项目用Activiti 5/6,迁移到7的曲线比想象中平缓,核心Service接口大体保留,只是引擎配置方式变了。
当然,如果你的项目规模已经到了需要多个流程引擎实例、需要可视化管理后台、需要复杂的租户隔离,那Camunda可能是更好的选择。但“SpringBoot 集成 Activiti 7 工作流引擎”这个组合,对于绝大多数中小型项目的审批需求来说,是够用且不臃肿的。
1.2 Activiti 7 相比老版本的变化在哪里
如果你以前用过 Activiti 5 或 6,直接跳到7会有一个明显的感受:很多默认行为变了。
首先是引擎生产方式的改变。老版本通常会构建一个ProcessEngine,然后从里面拿各个Service。Activiti 7 引入了更细粒度的ProcessEngineConfiguration,默认情况下SpringBoot自动配置会帮你创建好引擎,你只要往Bean容器里注入RepositoryService、RuntimeService、TaskService就能用。这一点集成感受是“无感”的,很像SpringBoot对DataSource的处理。
其次是模块拆分。Activiti 7 把原先的activiti-engine拆成了activiti-engine、activiti-spring、activiti-spring-boot-starter等模块,职责更清晰。如果你只想在非Spring环境里用引擎,也可以纯Java方式初始化。
第三是对Spring Boot 2.x的支持。Activiti 7.1.0.M系列官方测试过的组合是Spring Boot 2.x + JDK 8/11。很多人一上来就上Spring Boot 3 + JDK 17,结果发现各种Bean注入异常、MyBatis版本冲突,最后只能降级。我的建议是:集成Activiti 7时,Spring Boot老老实实待在2.5~2.7之间,别为了追新版本给自己找麻烦。
从整体架构上看,Activiti 7 的设计更贴近“云原生”——分离了引擎与Spring,提供了更清晰的API边界,这对后续把流程引擎单独拆成微服务是极其友好的。
2. 环境准备与工程搭建
2.1 版本选型:JDK、Spring Boot、Activiti 7 搭配推荐
过一遍版本搭配,这是整个集成过程中最容易被忽视、却最容易翻车的一步。我比较推荐的一套组合是:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 8 或 11 | 生产环境JDK8居多,JDK11也没问题 |
| Spring Boot | 2.5.x 或 2.7.x | 2.7系列是2.x的收尾版本,稳定,资料多 |
| Activiti 7 | 7.1.0.M6 | M6在社区用得最多,虽然不是正式版,但实际项目里扛得住 |
| MySQL | 5.7 或 8.0 | 5.7最稳,8.0注意时区与连接驱动 |
| Maven | 3.6+ | 常规要求 |
这里要特别提醒一下:Activiti 7.1.0.M6是里程碑版本,有些人看到“M”就慌,实际用下来只要不碰太冷门的功能,稳定性是可以放心的。反而一些所谓的正式版、SR版本,在Spring Boot 2.7组合下还出现过莫名其妙的兼容问题。社区里大家默认都在用M6,踩坑记录也多,遇到问题好查。
至于Spring Boot 3,我只能说目前Activiti官方支持力度不够,网上那些硬切Spring Boot 3的文章基本都有一段艰难的源码修改过程。如果你不是有强制要求,完全没必要冒险。
2.2 最小依赖与数据源配置
SpringBoot集成Activiti 7的依赖非常干净。在pom.xml里引入Starter即可:
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency>注意,这个Starter会把引擎核心、Spring集成、MyBatis持久层全部带进来,不需要额外再引activiti-engine。数据库驱动你需要自己加,比如MySQL:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>然后是application.yml配置。最小可用配置长这样:
spring: datasource: url: jdbc:mysql://localhost:3306/activiti?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver activiti: database-schema-update: true db-history-used: true history-level: full check-process-definitions: true几个配置项要理解到位:
database-schema-update: true表示启动时自动检查并创建Activiti所需的25张ACT_开头的表。第一次跑项目时,它就是帮你自动建表的开关。生产环境我更建议改成false,提前用脚本手动建表,避免应用权限过大。Activiti官方默认值其实是true,开发阶段保留即可。db-history-used: true决定是否写入历史表。如果你不需要流程追溯、审批历史查询,可以设false,但绝大多数审批系统都需要历史记录,所以建议开。history-level: full表示完整记录流程实例、活动实例、任务实例的详细信息。还有none、audit等级别,日常业务选full没毛病。check-process-definitions: true是启动时自动扫描并部署classpath:/processes/目录下的BPMN文件。如果你没有这个目录,启动时可能报“没有找到流程定义”的警告,但不会中断。
这里有个小坑:MySQL 8.0下连接串务必加nullCatalogMeansCurrent=true,否则Activiti 7启动时去获取表结构元数据会拿到全库的表,轻则启动告警,重则表结构校验失败。这个参数在官方文档里写得很隐晦,是我实际排查了半天才定位到的。
工程搭好之后,启动SpringBoot应用,控制台会输出创建ACT_开头的表结构SQL日志,正常看到这些表,说明集成已经成功了一大半。
3. 流程设计:请假审批的核心场景
3.1 BPMN 2.0 入门:流程定义、节点、网关
要说Activiti,绕不开BPMN 2.0。你不用把它想得太复杂,它本质就是一套描述流程的XML规范:谁在什么时候做什么事,做完之后下一个节点是谁。Activiti引擎负责解析这份XML并驱动它运行。
几个核心概念先理清:
- 流程定义:一个
.bpmn20.xml文件就是一个流程定义,它有唯一的id和name。对应数据库表是ACT_RE_PROCDEF。 - 流程实例:流程定义的一次具体执行。就像“请假流程”是一个模板,而“张三请假”就是一个流程实例。对应
ACT_RU_EXECUTION和ACT_HI_PROCINST。 - 节点:事件(开始、结束)、任务(用户任务、服务任务)、网关(排他、并行、包容)。
- 连线:也就是
sequenceFlow,指定节点的流转方向,可以带条件。
理解这四点,你基本就能看懂一个BPMN文件了。流程定义是静态的,流程实例是动态的,任务是通过ACT_RU_TASK实时跟踪的。这就解答了为什么不用自己设计状态字段:Activiti已经把流程状态、当前节点、历史记录全部用表结构帮你管理好了。
3.2 手写请假流程BPMN文件
我建议所有初学者都从手写BPMN开始,不要第一版就上可视化建模工具。手写能逼你去理解每个标签的含义。下面是我在项目中常用的请假审批流程,核心节点包括:开始、填写申请、经理审批、人事备案、结束。
<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" targetNamespace="http://www.example.com/process/leave"> <process id="leaveProcess" name="请假审批流程" isExecutable="true"> <startEvent id="startEvent" name="开始"/> <userTask id="applyTask" name="填写请假申请" activiti:assignee="${applyUser}"/> <userTask id="managerApprove" name="经理审批" activiti:assignee="${managerUser}"/> <userTask id="hrRecord" name="人事备案" activiti:assignee="${hrUser}"/> <endEvent id="endEvent" name="结束"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="applyTask"/> <sequenceFlow id="flow2" sourceRef="applyTask" targetRef="managerApprove"/> <sequenceFlow id="flow3" sourceRef="managerApprove" targetRef="hrRecord"/> <sequenceFlow id="flow4" sourceRef="hrRecord" targetRef="endEvent"/> </process> </definitions>这份BPMN文件放到src/main/resources/processes/leave.bpmn20.xml里。文件名后缀必须是.bpmn20.xml或者.bpmn,Activiti 7 自动部署时才认识。
文件里几个容易被忽略的细节:
activiti:assignee="${applyUser}"表示这个用户任务的办理人从流程变量applyUser里取。这里用的是Spring表达式,稍后在启动流程实例时把办理人传进去。isExecutable="true"必须设置,否则Activiti会认为这是不可执行的流程定义,部署时可能不加载。- 流程定义里
id是引擎唯一标识,name是展示名称。数据库里ACT_RE_PROCDEF.KEY_字段存的就是id,启动流程时要用它。
要扩展驳回分支时,只要在managerApprove之后加一个排他网关:
<exclusiveGateway id="managerJudge"/> <sequenceFlow id="flow5" sourceRef="managerApprove" targetRef="managerJudge"/> <sequenceFlow id="flow6" sourceRef="managerJudge" targetRef="hrRecord"> <conditionExpression xsi:type="tFormalExpression">${approved == true}</conditionExpression> </sequenceFlow> <sequenceFlow id="flow7" sourceRef="managerJudge" targetRef="applyTask"> <conditionExpression xsi:type="tFormalExpression">${approved == false}</conditionExpression> </sequenceFlow>条件表达式基于流程变量approved判断,引擎会按顺序匹配第一个为true的连线。这个就是审批系统中最常见的“同意走下一步、驳回退回到申请节点”的实现。
4. 核心代码实现:部署、启动、审批全链路
4.1 自动部署机制与 RepositoryService 使用
Activiti 7 最省事的一点就是自动部署。只要把BPMN文件放到classpath:/processes/目录下,SpringBoot应用启动时就会自动执行部署动作。部署简单理解就是把XML文件解析、校验、写入数据库的ACT_RE_*表中,后续引擎执行流程时去这些表里读取流程定义。
这个自动部署的类叫ProcessEngineAutoDeployment,它的行为依赖spring.activiti.check-process-definitions=true。如果设置成false,启动时就不会扫描processes目录。所以如果你发现自己的BPMN文件改了但启动不生效,第一件事就是检查这个配置项。
虽然自动部署很方便,但有些场景你还是需要手动用RepositoryService操作:
@Service public class ProcessDeployService { @Autowired private RepositoryService repositoryService; public Deployment deployFromClasspath(String bpmnPath) { return repositoryService.createDeployment() .addClasspathResource(bpmnPath) .name("手工部署流程") .deploy(); } public List<ProcessDefinition> listProcessDefinitions() { return repositoryService.createProcessDefinitionQuery() .latestVersion() .list(); } }我实际项目中是两种方式混着用的:开发阶段靠自动部署,快速迭代流程;生产环境靠手动部署,特别是流程有变更时,我会写一个管理接口,上传BPMN文件并执行部署操作,控制版本上线节奏。
版本管理方面,ACT_RE_PROCDEF表里同一个KEY_字段会有多个版本,VERSION_递增。启动流程实例时默认用最新版本,老版本不会影响运行中的实例。这一点设计得非常好,线上流程迭代可以做到平滑过渡。
4.2 启动流程实例与流程变量
部署完流程定义之后,真正让流程“跑起来”的是RuntimeService。启动一个流程实例,核心要做的就是三件事:指定流程定义Key、指定业务Key、设置流程变量。
@Service public class LeaveProcessService { @Autowired private RuntimeService runtimeService; public String startLeaveProcess(String userId, String managerId, String hrId, String reason, Integer days) { Map<String, Object> variables = new HashMap<>(); variables.put("applyUser", userId); variables.put("managerUser", managerId); variables.put("hrUser", hrId); variables.put("reason", reason); variables.put("days", days); ProcessInstance processInstance = runtimeService .startProcessInstanceByKey("leaveProcess", userId, variables); return processInstance.getId(); } }startProcessInstanceByKey的第一个参数是BPMN文件里的process id;第二个参数是业务Key,我习惯传业务主体的唯一标识,比如用户ID、申请单号,这样可以通过业务Key反查流程实例;第三个参数是流程变量Map,引擎会把它们存入ACT_RU_VARIABLE表。
这里有个重要的工程经验:流程变量不要放太多、太重的东西。虽然它支持存对象,但会序列化到数据库,如果放一个大的业务对象,一是影响性能,二是后续反序列化时类结构变了就可能报错。更合理的做法是只放业务主键、审批结论这类轻量字段,完整业务数据仍放在自己的业务表里,通过业务Key关联。
启动流程后,ACT_RU_TASK表里会立即插入第一条待办任务,也就是“填写请假申请”节点。这个节点因为没有实际业务动作,严格说可以设计成直接在代码里“假装完成”,但我更建议让它真实流转,这样历史记录里能完整体现“申请人提交”和“经理审批”两个动作。
4.3 任务查询与审批操作
任务查询和审批是整个工作流引擎使用频率最高的两个动作。先说查询待办,核心API是TaskService.createTaskQuery():
public List<Task> queryTodoTasks(String userId) { return taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime() .desc() .list(); }这里taskAssignee匹配的是BPMN里activiti:assignee指定的办理人。如果你按用户ID赋值,这里就能直接查出来。如果你用了候选组,也就是activiti:candidateGroups,查询方式要换成taskCandidateGroup。
审批完成任务就更简单了,taskService.complete(taskId, variables):
public void completeTask(String taskId, boolean approved, String comment) { Map<String, Object> variables = new HashMap<>(); variables.put("approved", approved); variables.put("comment", comment); taskService.complete(taskId, variables); }这个方法做的事是:把当前任务标记为已完成,写入历史表ACT_HI_TASKINST,然后根据BPMN连线的条件表达式,计算出下一个节点,生成新的待办任务。如果连线条件判断不满足任何一条,流程会卡住,节点状态停留在当前任务,这在调试时经常遇到。
如果你需要“驳回”操作,最简方式就是在完成任务时设置approved=false,让排他网关分支走到申请节点。这个方案的好处是侵入性小,审批逻辑完全由BPMN表达;缺点是每次驳回都会创建一个全新的申请任务,申请历史上会出现多个“填写请假申请”的任务实例,这其实是符合真实业务的:一次申请、两次提交,就是两条任务记录。
4.4 历史数据查询与报表统计
流程跑完还不够,审批记录、流程追溯、超时统计都依赖历史数据。Activiti 7 的历史查询通过HistoryService实现:
public List<HistoricProcessInstance> queryFinishedProcess(String userId) { return historyService.createHistoricProcessInstanceQuery() .startedBy(userId) .finished() .list(); } public List<HistoricActivityInstance> queryProcessHistory(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); }历史相关数据存在ACT_HI_*表中,例如:
ACT_HI_PROCINST:流程实例历史,包含开始时间、结束时间、持续时间、业务Key。ACT_HI_TASKINST:任务历史,包含每个节点的办理人、开始时间、结束时间。ACT_HI_ACTINST:活动实例历史,更细粒度记录了每个节点的执行轨迹。ACT_HI_VARINST:流程变量历史,保存每次更新的变量值。
实际做审批中心报表时,我一般不会直接查这些表,而是通过HistoryService封装一层,再按业务需求聚合。比如统计某个部门平均审批耗时,就可以查出所有流程实例的START_TIME_和END_TIME_做差值。需要注意,历史表数据量会持续增长,必须有定期清理策略,这一点我在后面章节专门说。
5. 一次真实的联调踩坑记录
5.1 表结构没有自动创建
这是我第一次接入时遇到的问题:SpringBoot应用正常启动,日志里没有报错,Activiti相关的表却一张都没有。查了半小时,发现问题出在数据源连接串上——MySQL 8.0下没有加nullCatalogMeansCurrent=true,Activiti 7查询数据库元数据时把整个实例的所有库都当成了自己的catalog,导致表存在性判断被干扰。加上这个参数后,应用启动时正确执行了建表脚本。
其次是权限问题。如果数据库账号没有DDL权限,建表SQL会执行失败,但异常信息不一定很明显,可能只是控制台打出一长串SQL之后被吞掉了。排查方式是在配置里临时打开spring.jpa.show-sql对应MyBatis的SQL日志,看建表语句是否真的执行成功。更省事的验证方式是:启动前用一个有DDL权限的账号,或者干脆先手动执行Activiti 7自带的activiti.mysql.create.engine.sql脚本。
5.2 流程图中文乱码与BPMN文件不自动部署
中文乱码是老生常谈,但每次换环境都有人踩。两个层面注意:一是BPMN文件本身的编码,所有文件统一保存为UTF-8;二是JVM默认文件编码,如果启动参数没指定-Dfile.encoding=utf-8,在Windows中文环境下很容易出现流程名称和节点名称乱码。解决方案是把file.encoding参数加到启动脚本里,并确保MySQL连接串里带上characterEncoding=utf8。
BPMN文件不自动部署,除了前面说的check-process-definitions配置项之外,还有一个容易忽略的点:文件名后缀。如果你把文件命名为leave.xml,Activiti 7默认扫描器是不认识的,必须用.bpmn20.xml或.bpmn后缀。我自己之前就把文件命名为leave-process.xml,启动后流程始终不见,后来翻官方文档才发现扫描器的文件名匹配规则。
5.3 多实例会签/或签的实现心得
审批场景做到中后期,会签一定是绕不开的需求。一个合同可能既要法务审,又要财务审,还要总经理批。Activiti的多实例(multiInstance)机制就是专门干这个的。下面是一个会签节点的示例:
<userTask id="multiApprove" name="多人会签" activiti:assignee="${assignee}"> <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${approverList}" activiti:elementVariable="assignee"> <completionCondition>${nrOfCompletedInstances >= nrOfInstances}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>isSequential="false"表示并行审批,approverList是要参与审批的人员列表,assignee是循环取出的当前办理人。这个配置下,引擎会为approverList里的每个人生成一个独立的任务实例,全部完成后才继续往下流转。
如果想要“或签”效果——即一人审批通过即可流转——把completionCondition改成${nrOfCompletedInstances >= 1}就行。实际项目中甚至可以做更复杂的占比判断,比如${nrOfCompletedInstances / nrOfInstances >= 0.6}表示最少六成人同意才能通过。这个表达式的灵活性很高,但注意它是在循环体内计算的,语法错误会在运行时表现成节点不流转,调试时一定要借助ACT_HI_ACTINST看活动实例状态。
5.4 与Spring Security认证体系对接的取舍
Activiti 7自带了一套用户组模块(ACT_ID_*表),但说实话,实际项目里我不建议直接用它来管用户。几乎所有SpringBoot项目的用户体系都是自己建的,再做一套用户同步会很别扭。
更合理的做法是:流程引擎只认“用户ID字符串”,也就是你在activiti:assignee里填的变量值。自己系统的用户表里维护用户ID,启动流程时把当前登录人的ID传入流程变量,查询待办时也用当前登录人ID去taskAssignee查询。谁登录谁传谁查,完全不需要Activiti的身份模块介入。
如果你非要用Activiti的IdentityService来关联真实用户,也不是不行,但要在用户注册、离职、角色变更时做同步逻辑,很啰嗦。我的建议是:优先把Activiti当成一个“无身份状态”的流程状态机,用户维度全部由业务系统负责。
6. 进阶优化:性能、数据清理与流程解耦
6.1 常见性能瓶颈与索引优化
Activiti 7 的默认建表脚本已经给所有外键和常用查询字段建了索引,但业务跑起来后,还是会遇到几个需要手动优化的点。
最明显的是ACT_RU_TASK表。待办任务表是所有审批入口的汇聚点,随着流程数据增长,createTaskQuery().taskAssignee(userId)会慢慢变慢。我一般会给这张表的ASSIGNEE_列建一个组合索引,再带上CREATE_TIME_做排序优化。如果任务量大,还可以考虑把ACT_RU_TASK和ACT_RU_EXECUTION表做周期性的历史归档。
其次是历史表ACT_HI_ACTINST。这张表记录每个流程每个节点的执行记录,数量级是所有表里最大的。它虽然有PROC_INST_ID_的索引,但按ACT_ID_和START_TIME_查询时经常走全表扫描。我的做法是加一个(PROC_INST_ID_, ACT_ID_, START_TIME_)的联合索引,统计类查询会舒服很多。
最后说一点:不要在生产环境开着database-schema-update=true。一旦引擎升级或表结构校验条件变化,可能会触发执行DDL,造成意料之外的表结构变更。更稳健的做法是用官方脚本建好表,之后设置成false。
6.2 历史数据增长与定期清理策略
历史表不像业务表,它可以无限增长,因为每个流程实例的每个节点都会有记录。我之前一个项目跑了8个月,ACT_HI_ACTINST表到了1.2亿行,导致历史查询巨慢,最后不得不做数据归档。
清理主要分两种思路:
一是按时间删除。比如只保留最近12个月的历史数据,直接执行DELETE。这种方式简单粗暴,但要注意历史表和运行时表之间有外键关联,删除时要按顺序来,先删明细、再删主表,否则会因外键约束报错。
二是做冷热分离。定期把超过N个月的ACT_HI_*数据迁移到另一张归档表或者另一个库,业务层面上历史审批记录还能查到,但对在线交易系统的查询性能没有影响。这个方案实现成本更高,但更适合对审计链路有严格要求的场景。
我实际推荐的做法是:短时间内用方案一兜底,上线稳定后再做方案二。不过无论哪种方案,删除前都建议先备份,并且选在业务低峰期执行。
6.3 流程引擎与业务系统解耦的实践
流程引擎用久了,最容易出现的坏味道就是把太多业务逻辑塞进BPMN的serviceTask里,或者反过来在Java代码里写一堆流程节点判断。我的经验是:流程引擎只负责“状态流转”和“任务分配”,业务规则留在业务服务层。
比如审批通过后要发通知、更新订单状态,这些不应该放在Activiti的监听器里大段调用。正确做法是:审批完成后把业务Key和审批结果告诉业务模块,由业务模块自己去处理后续动作。这块我会借助Spring的事件机制,在审批完成时发一个业务事件,业务侧监听处理,这样流程引擎和业务逻辑之间只通过事件对话,耦合降到最低。
另外,流程变量的命名要做规范。别用a、b这种名。我习惯定义一组常量类,把流程变量名全部管起来,比如:
public final class ProcessVariables { public static final String APPLY_USER = "applyUser"; public static final String MANAGER_USER = "managerUser"; public static final String APPROVED = "approved"; }这样在启动流程、完成任务、网关条件表达式里都用同一个常量,避免拼错字符串导致网关走错分支。这个问题排查起来非常隐蔽,只看日志很难发现是变量名拼错还是条件写错。
最后再分享一个小技巧
排查流程不流转的问题时,我最常用的手段不是看代码日志,而是直接查ACT_HI_ACTINST表。这张表会把每个节点的进入时间、离开时间、结束时间都记下来。某个节点卡住不动,看一下这张表,你就知道流程停在了哪个节点、在这个节点上待了多久。这比在代码里打几百行日志高效得多。
从第一次在SpringBoot里集成Activiti 7成功跑通请假流程,到后来支撑几十个复杂业务流程,这个组合给我的整体感受是:入门不难,踩坑不少,但只要把BPMN流程设计、核心Service API、流程变量管理这几块理解透了,后面做任何审批类需求都会很顺。这篇文章里写的都是我在实际项目中验证过的方案和习惯,希望能帮你少走几步弯路。如果你在集成的某个环节卡住了,欢迎带着具体的报错或者流程定义来聊,我这边积累了不少疑难案例,大概率能帮你定位到问题。