
团队里新来的同事看我在跑一个审批流程问了一句“这不就是请假状态机吗加一摞 if else 不也能做”我笑了笑说实话很久以前我也这么想。可当你碰到的业务从“提交-审批”变成五级审批、会签、驳回、加签、流程版本升级的时候状态机代码会让你怀疑人生。后来我把流程定义从业务代码里彻底剥离出来交给 Activiti 7 去管SpringBoot 只负责对接接口世界一下清爽了。这篇文章不是照抄官方文档。我按实际落地顺序把 SpringBoot 集成 Activiti 7 的选型思路、依赖与配置、BPMN 建模、核心 Service 使用、25 张表的数据流转以及我一路上踩过的坑都整理一遍。适合准备做审批流、任务流、工单系统的后端同学参考尤其适合被“代码写死审批链路”折磨过的人。1. 为什么选工作流引擎先解决“代码写死流程”的问题1.1 状态机做审批最痛的三个地方我最早参与的一个项目审批链路是用订单状态字段硬顶的。状态枚举十几个每个 Service 方法里都是switch (status)流程一变就要改代码、发版、回滚而且历史操作记录基本靠业务日志硬凑。最离谱的一次客户说“加一个财务复核节点”前后改了三张表、五个接口、两处定时任务。那次之后我就明白审批流属于“高频变化、强规则、有痕迹要求”的业务根本不适合用业务代码去维护。工作流引擎做的事情很简单把流程定义谁先审、谁后审、什么条件下走哪个分支变成一份独立的模型文件运行时的任务推进由引擎调度业务代码只关心当前待办、提交审批、查询历史这些动作。流程改动只需要调整模型重新部署不再动业务代码。1.2 Activiti 7 和之前版本到底差在哪Activiti 7 相比 5.x 和 6.x比较大的变化是模块化重构和云原生定位。7.x 把引擎拆成更清晰的模块和 SpringBoot 的集成也顺势成了默认路径starter 直接引入就能跑。官方把很多内部组件做了裁剪同时保留了核心的 BPMN 2.0 流程引擎能力还强调了对 CMMN案例管理和 DMN决策表的支持。对于大部分后端项目来说BPMN 部分是最常用的DMN 可以让你把审批规则从 if else 里拆出来CMMN 则是做任务型工单系统时才能发挥价值。同类的开源引擎也有几个衍生分支但我个人选择 Activiti 7 的原因很朴素SpringBoot 生态融合顺畅、社区资料多、API 设计直观出一个问题基本都能搜到解决方案。做技术选型别光看“哪个更强”要看“哪个你团队能最快上手”。Activiti 7 的学习曲线在同类引擎里属于中等偏下对国内团队尤其友好。1.3 集成前先想清楚引擎管什么业务管什么这是很多人第一步就搞混的。Activiti 只管流程定义、任务分配、节点流转、历史记录它不管你的业务数据长什么样也不关心审批单上的金额、天数、附件怎么存。正确做法是流程启动时把业务主键作为 BusinessKey 传进去审批单详情继续走你自己的业务表任务表里只留流程实例 ID、任务节点、处理人等运行态数据。引擎负责“事情做到哪一步了”业务表负责“事情本身是什么”。这个边界想清楚后面所有代码都顺了。2. 集成前置准备版本组合、依赖与配置2.1 版本组合是第一个大坑Activiti 7 的版本选择一句话总结别乱追新先锁定 SpringBoot 版本再看兼容性。我在项目里用的是 SpringBoot 2.5.x Activiti 7.1.0.M6这个组合在生产环境跑了很久比较稳。Activiti 7 对 SpringBoot 3.x 的支持需要对应新版本而且相关配置项有变化如果没有特别需求保守选 SpringBoot 2.x 更省心。数据库方面MySQL 5.7 和 8.0 都试过问题不大。JDK 用 8 或 11 都行Activiti 7 官方对 Java 8 兼容没问题。有一点要注意Activiti 7 内部依赖 MyBatis不需要你再额外引入 MyBatis 的 starter否则可能出现版本冲突或 SQL 会话工厂覆盖问题。2.2 依赖引入一个 starter 搞定Maven 依赖很简单核心就一个dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency引入 starter 后Activiti 会自动创建数据源相关的引擎配置。如果你项目里已经有自己的数据源不用担心引擎会复用 Spring 容器里的 DataSource。但要注意Activiti 7 的 starter 默认强依赖 Spring Security这一点很多人不知道启动起来各种报错后面我单独讲。2.3 配置文件里的几个关键参数application.yml中除了常规数据源配置还需要配置 Activiti 的核心行为spring: datasource: url: jdbc:mysql://localhost:3306/activiti_demo?useUnicodetruecharacterEncodingutf8nullCatalogMeansCurrenttrue 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: false逐个解释一下这几个参数的用途database-schema-update: true启动时自动建表或更新表结构。第一次跑时必须开启生产环境建议改成false由 DBA 统一管理表结构变更。db-history-used: true开启历史表记录审批流必须有痕迹这个必开。history-level: full记录所有历史细节包括变量变更、表单字段等。如果不做审计audit级别也行但别用none否则历史实例查不到。check-process-definitions: false关闭启动时自动扫描部署classpath:/processes/下的流程文件。这个参数很多人没注意后面自动部署重复的问题就出在它身上。连接 MySQL 时URL 里的nullCatalogMeansCurrenttrue是个老坑。不加这个引擎启动时可能报“表不存在”或错误地访问了别的库的同名表。数据库连接池建议用 HikariCPSpringBoot 默认就是它不用额外配置。3. 流程建模从 BPMN 文件到可执行流程3.1 先画一个最常用的请假审批流程Activiti 的流程定义文件是 BPMN 2.0 标准的 XML。用 IDEA 的插件画图很方便也可以直接手写 XML。以请假流程为例核心结构大致是开始事件 - 填写申请 - 直属主管审批 - 经理审批 - 结束天数超过 3 天需要总经理审批。?xml version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:activitihttp://activiti.org/bpmn targetNamespacehttp://www.activiti.org/test process idleaveProcess name请假流程 isExecutabletrue startEvent idstartEvent name开始/ userTask idapplyTask name填写请假申请 activiti:assignee${applyUser}/ userTask idleaderTask name主管审批 activiti:assignee${leaderUser}/ userTask idmanagerTask name经理审批 activiti:assignee${managerUser}/ exclusiveGateway idgateway name天数判断/ userTask idgmTask name总经理审批 activiti:assignee${gmUser}/ endEvent idendEvent name结束/ sequenceFlow idflow1 sourceRefstartEvent targetRefapplyTask/ sequenceFlow idflow2 sourceRefapplyTask targetRefleaderTask/ sequenceFlow idflow3 sourceRefleaderTask targetRefmanagerTask/ sequenceFlow idflow4 sourceRefmanagerTask targetRefgateway/ sequenceFlow idflow5 sourceRefgateway targetRefgmTask conditionExpression xsi:typetFormalExpression![CDATA[${days 3}]]/conditionExpression /sequenceFlow sequenceFlow idflow6 sourceRefgateway targetRefendEvent conditionExpression xsi:typetFormalExpression![CDATA[${days 3}]]/conditionExpression /sequenceFlow sequenceFlow idflow7 sourceRefgmTask targetRefendEvent/ /process /definitions这段 XML 你不需要背关键在于理解几个元素startEvent是起点userTask是人工审批任务exclusiveGateway是排他网关按条件选择一个分支往下走sequenceFlow就是节点之间的连线。流程文件放在src/main/resources/bpmn/leave.bpmn20.xml这种位置命名规范建议.bpmn20.xml结尾。3.2 审批人三种写法固定值、候选人、表达式userTask上最核心的就是审批人配置这个直接决定你代码怎么写。第一种固定写死某个用户 IDuserTask idleaderTask name主管审批 activiti:assigneezhangsan/这种做法适合演示生产环境别用审批人是会离职、调岗的。第二种候选人组userTask idleaderTask name主管审批 activiti:candidateUserszhangsan,lisi/activiti:candidateUsers activiti:candidateGroupsmanager_group/activiti:candidateGroups /userTask候选人模式下任务不会直接出现在某个人的待办里而是进入候选池需要用户调用taskService.claim(taskId, userId)先拾取任务再审批。这个模式适合“抢单制”任务队列。第三种也是我推荐的方式用表达式从流程变量里动态取审批人userTask idleaderTask name主管审批 activiti:assignee${leaderUser}/流程启动时传入leaderUser的值引擎在创建任务时会自动解析为实际审批人。这样审批人可以由你的业务接口动态计算比如“查找发起人的直属上级”作为流程变量传进去。动态表达式还可以配合网关条件做复杂的审批规则非常灵活。3.3 网关条件表达式怎么写网关分支的条件表达式实际就是 Spring 表达式语言加流程变量。比如上面的请假流程中days就是流程变量发起流程时通过MapString, Object variables传入。条件的优先级是从上到下匹配排他网关只走第一个满足条件的分支。这是最简单也最常用的网关类型如果你需要“几个分支同时走”要换成并行网关parallelGateway会签就是并行网关 多实例任务的组合用法后面单开一节讲。4. 核心 API 与业务代码从部署到审批全流程实现4.1 五个必须认识的 ServiceActiviti 7 的 API 设计很清晰核心就五个 Service记住它们干什么就够了。Service作用典型方法RepositoryService管理流程定义、部署包、BPMN 模型createDeployment()、createProcessDefinitionQuery()RuntimeService启动流程实例、管理运行态数据startProcessInstanceByKey()、createProcessInstanceQuery()TaskService查询待办、完成任务、认领任务createTaskQuery()、complete()、claim()HistoryService查询已结束流程、历史任务、历史变量createHistoricProcessInstanceQuery()ManagementService引擎管理、任务执行、定时作业维护executeJob()、createTablePageQuery()日常开发 90% 的时间都在前四个 Service 之间打转。还有两个辅助 ServiceIdentityService管用户和组FormService管表单数据由于我们通常用业务系统自己的用户体系这两个基本不会直接用。4.2 流程部署别让引擎自动扫描流程文件改完后需要部署到引擎。部署的本质就是把 BPMN 文件内容解析成流程定义存进ACT_RE_PROCDEF表生成版本号。我建议关掉自动部署就是上面配置里的check-process-definitions: false改成手动部署原因后面讲“重复部署”坑时会细说。手动部署代码如下Autowired private RepositoryService repositoryService; public void deploy(String bpmnPath) { repositoryService.createDeployment() .addClasspathResource(bpmnPath) .name(请假流程v1) .deploy(); }部署一次生成一个ACT_RE_DEPLOYMENT记录。相同process id的流程再次部署会生成新的版本旧的流程实例不受影响新流程实例默认走最新版本。这个机制很关键流程升级不会把跑了一半的审批单搞乱。4.3 发起流程绑定业务主键发起流程是引擎与业务系统衔接的第一站。核心方法就是startProcessInstanceByKeykey是 BPMN 文件里process idleaveProcess的 id。public ProcessInstance startProcess(String processKey, String businessKey, MapString, Object variables) { return runtimeService.startProcessInstanceByKey(processKey, businessKey, variables); }第二个参数businessKey一定要传传你的业务单据主键。这样以后你能通过业务单号反查流程实例比如查历史记录、催办、统计耗时都靠这个关联。这里有个经验流程变量variables别一股脑往里塞。引擎会把流程变量存进ACT_RU_VARIABLE表塞太多大对象会影响性能。只传流程流转需要的数据比如审批人、金额、天数、部门 ID业务详情数据通过 BusinessKey 从业务表里查。流程启动后ProcessInstance对象里有processInstanceId这个 ID 就是整个流程生命周期贯穿始终的唯一标识。在业务表里加一个process_instance_id字段存它后面所有查询都走这个关联。4.4 查询待办和完成任务查询某人的待办用 TaskServicepublic ListTask getTodoTasks(String userId) { return taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime().desc() .list(); }这里有个细节如果你的流程节点设置了candidateUsers或candidateGroups待办查询要区分是“指派给我的任务”还是“我可以认领的任务”。前者用taskAssignee后者用taskCandidateUser两者查询结果不一样。完成任务就是completepublic void completeTask(String taskId, MapString, Object variables) { taskService.complete(taskId, variables); }taskId不是流程实例 ID而是ACT_RU_TASK表的主键。完成时传入的variables会作为后续网关判断和下一步节点审批人解析的依据。比如请假流程的days变量发起时传一次后续环节如果还要用就在完成时再次传入否则网关条件可能取不到值。4.5 一个完整的业务接口封装示例实际项目中我不会在 Controller 里直接调用引擎 API而是包一层流程服务把引擎调用收口在一起。下面是一个简化但完整的封装思路Service public class LeaveProcessService { Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; Autowired private HistoryService historyService; Autowired private SecurityUtil securityUtil; public static final String PROCESS_KEY leaveProcess; public void startLeave(String businessKey, String applyUser, String leaderUser, String managerUser, Integer days) { securityUtil.logInAs(applyUser); MapString, Object variables new HashMap(); variables.put(applyUser, applyUser); variables.put(leaderUser, leaderUser); variables.put(managerUser, managerUser); variables.put(days, days); runtimeService.startProcessInstanceByKey(PROCESS_KEY, businessKey, variables); } public ListTask queryTodo(String userId) { return taskService.createTaskQuery().taskAssignee(userId).list(); } public void approve(String taskId, boolean approved, MapString, Object extraVars) { MapString, Object vars extraVars null ? new HashMap() : extraVars; if (!approved) { vars.put(approved, false); // 驳回逻辑见 4.6 } else { vars.put(approved, true); } taskService.complete(taskId, vars); } }这里的SecurityUtil是 Activiti 7 集成 Spring Security 后必须的一个辅助类。因为引擎在创建任务、更新任务时需要知道当前操作人是谁这个身份就来自 Spring Security 的上下文。后面会专门讲怎么做。4.6 审批通过和驳回引擎不帮你解决的坑Activiti 自带的原生 API 支持“通过”因为complete之后流程就往下走了。但“驳回”是业务场景里的高频需求官方却没有现成的一行代码让你直接跳到上一个节点必须自己实现跳转。我常用的一种驳回实现思路是基于ChangeActivityStateBuilder的活动节点跳转public void reject(String taskId, String targetTaskKey) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); String processInstanceId task.getProcessInstanceId(); ListExecution executions runtimeService.createExecutionQuery() .processInstanceId(processInstanceId) .activityId(task.getTaskDefinitionKey()) .list(); for (Execution execution : executions) { runtimeService.createChangeActivityStateBuilder() .moveExecutionToActivityId(execution.getId(), targetTaskKey) .changeState(); } // 记录驳回原因 taskService.addComment(taskId, processInstanceId, 驳回); }这个方法的核心是让当前节点所在的执行实例从“活动节点 A”挪到“目标节点 B”。挪过去之后流程引擎会在目标节点重新生成一个新的 UserTask指定审批人还是按节点上的表达式解析所以被驳回后重新生成的待办会回到上一个审批人名下。但这里必须提醒一下节点跳转 API 在会签节点、并行网关内部的执行语义会比较复杂执行实例可能有多个分支跳转会碰到“多个执行实例”的情况。我的建议是驳回场景先只支持串行审批链路会签和并行分支的驳回要么做限制要么用流程版本重新发起的方式规避别硬跳。遇到复杂的驳回场景不少人会在流程里专门加一个“驳回”服务节点配合自定义注解来做这个方案更可控但也更复杂一般团队用到变更节点跳转就够了。5. 二十五张表和数据流转机制5.1 表前缀看一眼就懂Activiti 7 启动后会在数据库里生成 25 张表看着多实际上按前缀分四类就很好记前缀分类核心表说明ACT_RE_*流程定义相关RE 即 RepositoryACT_RE_DEPLOYMENT、ACT_RE_PROCDEF、ACT_RE_MODEL部署包、流程定义、模型设计ACT_RU_*运行时数据RU 即 RuntimeACT_RU_EXECUTION、ACT_RU_TASK、ACT_RU_VARIABLE、ACT_RU_IDENTITYLINK流程执行实例、待办任务、流程变量、身份关联ACT_HI_*历史数据HI 即 HistoryACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST、ACT_HI_VARINST历史流程实例、历史任务、历史节点、历史变量ACT_GE_*通用数据GE 即 GeneralACT_GE_BYTEARRAY、ACT_GE_PROPERTY二进制资源、引擎属性存储这里最容易让人困惑的是ACT_RU_TASK和ACT_HI_TASKINST的关系。简单理解任务没完成时在ACT_RU_TASK里任务一完成就“永久搬家”到ACT_HI_TASKINST运行时表只保留当前状态的“快照”历史表负责全部履历。5.2 一张请假单从发起到归档数据是怎么流转的我按最典型的请假流程走一遍你会对表的协作有直感部署 BPMN 时ACT_RE_DEPLOYMENT新增部署记录XML 内容存进ACT_GE_BYTEARRAY解析后的流程定义存进ACT_RE_PROCDEF。发起流程时ACT_RU_EXECUTION生成一条流程实例执行记录ACT_RU_TASK插入第一个待办任务填写申请流程变量写入ACT_RU_VARIABLEACT_HI_PROCINST同时插入一条历史流程实例记录。每完成一个任务对应的ACT_RU_TASK记录被删除ACT_HI_TASKINST补上一条带完成时间的历史任务记录ACT_HI_ACTINST记录节点活动起止时间。审批全部走完流程到达结束事件ACT_RU_EXECUTION的运行记录消失ACT_HI_PROCINST记录结束时间和结束状态。看到没有ACT_RU_*表其实是“暂时的”流程结束后数据就基本清空了真正长期留存的是ACT_HI_*历史表。所以做数据统计、流程耗时分析时主要查历史表。5.3 历史数据膨胀与清理生产环境跑一年历史表数据会涨得飞快尤其是ACT_HI_ACTINST和ACT_HI_VARINST。每过一次节点就插入好几行一个流程走六七步就是几十条历史记录。我见过有人把历史表当宝贝攒着结果数据库几百 G查询越来越慢。对比一下常见处理方案方案优点缺点定期删除 N 天前的历史流程实例表体积控制明显历史痕迹查不到审计不满足冷备归档到历史库/数仓保留痕迹主库瘦身需要额外开发归档任务按业务类型只清理非必要的明细历史兼顾审计和性能清理逻辑要写得很小心我个人更推荐第二种思路主库只保留半年内的热数据更早的数据归档到独立的历史库或数仓表。如果你只需要满足内部管理定时任务删除也许是够用的。但删除历史要格外小心ACT_HI_PROCINST删掉后关联的ACT_HI_TASKINST和ACT_HI_ACTINST如果不一起删就会变成孤儿数据。建议按PROC_INST_ID_关联分批删除。6. 常见问题与排查技巧实录6.1 启动报错排查速查表我在集成过程里遇到过不少报错整理成一张表基本覆盖新手启动阶段的绝大多数问题报错信息根本原因解决办法Table ACT_GE_PROPERTY doesnt exist数据库没建表或建表指向了错误的库确认database-schema-updatetrue确认数据源 URL 的nullCatalogMeansCurrenttrueNo bean named processEngine availablestarter 没引入成功或包冲突检查activiti-spring-boot-starter依赖排除多余的 mybatis starterUnsatisfiedDependencyException ... UserDetailsServiceActiviti 7 强依赖 Spring Security引入 security 依赖并配置内存用户见 6.2启动时流程文件自动部署重复每次重启都扫classpath:/processes/设置check-process-definitions: false改为手动部署中文节点名显示乱码引擎渲染流程图的字体不支持中文通过配置设置中文字体见 6.46.2 Activiti 7 与 Spring Security 的纠缠这个坑非常经典。Activiti 7 的 starter 默认引入了 Spring Security 相关依赖启动的时候会要求容器里存在UserDetailsService和AuthenticationManager。如果你项目没接 security启动直接飘红。最简单的解法是定义一个SecurityUtil内部用内存用户模拟登录身份Component public class SecurityUtil { public void logInAs(String username) { UserDetails userDetails createUserDetails(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } private UserDetails createUserDetails(String username) { User.UserBuilder builder User.withUsername(username); builder.password(not-used); builder.authorities(ROLE_ACTIVITI_USER); return builder.build(); } }然后在流程服务每个操作前先调用securityUtil.logInAs(当前操作人)引擎就会认为当前人是username任务查询和完成任务时能正确记录处理人。如果你的业务系统已经接了 Spring Security 做登录认证那不需要这套模拟逻辑引擎会自动拿到认证上下文里的用户。这里多说一句Activiti 7 这么设计不是故意折腾人是为了在引擎内部贯彻“谁操作、谁负责”的审计模型。理解这层意图你就不觉得它多余了。6.3 流程部署重复、版本混乱怎么办Activiti 有个机制流程部署后引擎会以部署包的资源名为维度判断是否已部署。如果你用check-process-definitionstrue每次启动都自动扫描classpath:/processes/那么同一个 BPMN 文件可能被部署多次ACT_RE_PROCDEF表里VERSION_一路累加。解决方式有两个方向。一个是给 BPMN 文件名加版本号每改一次就改文件名但这样文件名维护很累。另一个就是我前面推荐的手动部署 部署前检查public void deployIfNeeded(String bpmnPath, String processKey) { long count repositoryService.createProcessDefinitionQuery() .processDefinitionKey(processKey) .count(); if (count 0) { deploy(bpmnPath); } }启动时显式调用一次deployIfNeeded只在没有该流程定义时才部署能够避免重复版本堆积。版本本来是用来支持“灰度发布”的你是为了部署还是为了升级必须想清楚别让部署变成了无意义的版本刷屏。6.4 流程图片生成时中文乱码如果项目里有“查看审批流程图”的需求引擎默认生成 PNG 图片时会用 Java 的默认字体渲染中文字符就会变成方块。解决办法是在引擎配置里指定中文字体Configuration public class ActivitiConfig implements ProcessEngineConfigurationConfigurer { Override public void configure(SpringProcessEngineConfiguration config) { config.setActivityFontName(宋体); config.setLabelFontName(宋体); config.setAnnotationFontName(宋体); } }注意服务器上必须安装了对应字体没有的话要手动安装。这个配置类实现ProcessEngineConfigurationConfigurer接口会被引擎自动识别。如果你在容器里部署又希望减少服务器依赖也可以改成生成 SVG 或者直接把 BPMN XML 交给前端去解析渲染但一般情况下配好字体就够了。6.5 审批人变量解析失败任务停在“幽灵”节点有一种情况很妖流程发起成功了待办表却是空的。查ACT_RU_TASK才发现任务存在但ASSIGNEE_字段是空。多半是因为流程节点的activiti:assignee${leaderUser}表达式在运行时解析不到leaderUser这个流程变量。比如变量名拼写不一致、变量是在完成任务时才传入而发起时没传都会导致审批人获取不到。排查思路很简单一条 SQL 进ACT_RU_VARIABLE看当前流程实例有哪些变量再对照 BPMN 里表达式用到的变量名是否完全一致。这个坑不难排但很容易反复出现建议把流程节点的审批人表达式做成统一规范比如所有审批人都用${xxxUser}格式别混用assignee和candidate两种模型能减少很多莫名其妙的问题。7. 会签、并行网关和多实例任务再往上走一层7.1 排他网关和并行网关的区别一句话讲透排他网关是“多选一”条件从上到下匹配命中一个就走那个分支。并行网关是“全都要”它会创建多条执行分支所有分支都走到汇合节点后流程才继续。并行网关适合需要同时多人审批、多部门联动的场景。记住一个判断标准如果业务规则明确说“满足条件 A 就不走 B”用排他网关“需要 A、B 都完成后才进入下一步”用并行网关。7.2 会签怎么配置多实例任务会签是审批里最典型的高级用法“会签”就是多个审批人各自独立审批要么全部通过才继续或一票否决要么多数通过。Activiti 处理会签靠multiInstanceLoopCharacteristics实现。一个示例部门内多人同时会签。userTask idmultiApproval name部门会签 activiti:assignee${approver} multiInstanceLoopCharacteristics isSequentialfalse activiti:collection${approverList} activiti:elementVariableapprover/ /userTaskcollection是审批人列表的流程变量elementVariable是每次循环当前元素的变量名isSequential控制串行还是并行。会签任务会在ACT_RU_TASK里生成多条任务记录每人处理自己的那一条全部完成前这个节点不会继续往下走。会签在代码层面不需要额外 API发起流程时把approverList作为一个 List 变量传进去即可ListString approverList Arrays.asList(zhangsan, lisi, wangwu); variables.put(approverList, approverList);7.3 会签场景下驳回的复杂度会签节点驳回是 Activiti 开发中少数真正让人头秃的问题。因为会签节点内部有多个执行实例每个执行实例是独立的如果直接调用节点跳转 API你可能会发现跳转只移动了一个分支其他分支还卡在原节点流程状态变得很诡异。我的实际经验是会签节点的驳回不要用活动跳转硬做。要么设计流程时把会签拆成独立子流程子流程整体驳回要么只记录“驳回意见”等所有会签人审批完后由网关判断是否回到上级节点。这个方案牺牲了一些“立即驳回”的实时性但流程状态是整洁可控的。如果你对这块有兴趣先在小项目里把moveExecutionToActivityId的语义吃透再考虑接复杂场景别一上来就挑战会签驳回。8. 沉淀下来的几点实用建议跑通 SpringBoot Activiti 7 其实不难难的是让它长期稳定地服务业务。最后分享几个实操沉淀下来的建议都是踩过坑才明白的。第一流程引擎和业务系统的事务边界要想清楚。taskService.complete()和你的业务数据更新最好在同一个事务里否则会出现“审批单状态已更新引擎任务没走完”或者反过来“流程走了业务数据没存住”的脏状态。建议在 Service 方法上加Transactional让业务操作和流程操作一起提交或一起回滚。第二流程变量不是缓存别把大对象塞进去。我见过有人把整个审批表单 JSON 塞进variables结果ACT_RU_VARIABLE表膨胀得非常快。流程变量只留流程引擎需要的判断数据其他业务数据按 BusinessKey 关联到业务表查询。这是性能和可维护性双方面的要求。第三流程定义文件的修改要当成接口变更来管理。BPMN 文件改之前先想清楚存量流程实例怎么办新版本是向前兼容还是直接取代我建议引入版本号规范流程实例启动时指定版本或使用最新版升级前先跑一遍完整的“发起-审批-归档”回归用例尤其要回归网关表达式和审批人表达式是否还能正常解析。第四不要把引擎 API 散落到业务代码里。Controller 直接调runtimeService、taskService的代码短期看爽长期看想改一个流程名称都要全局搜索。正确的姿势是通过一个流程服务层统一封装Controller 只面向业务方法。这样以后换掉 Activiti或者在不同模块间迁移流程改动面都控制在服务层内部。Activiti 7 能做的事情远不止审批流任务编排、案例管理、规则决策这些场景它都有对应能力。但技术选型永远是匹配当前需求最重要。如果你的业务只是固定两三步审批状态机简单又可控真没必要上工作流引擎。可一旦流程开始变得灵活多变、审批链条越来越长流程引擎带来的收益会远大于学习成本。这就是我在这类项目上最真实的体会。