SpringBoot集成Activiti 7实战:工作流引擎配置、流程部署与排错指南 搞工作流这玩意儿第一步就很容易让人懵网上资料一堆但大部分是旧版本的老黄历照着抄完启动直接报错。我自己前前后后踩了不少坑从 Activiti 5 一直用到 7这中间版本差异真不是一般的大。这篇就把 SpringBoot 集成 Activiti 7 这件事从头到尾捋一遍不讲虚的全是能直接落地的配置、代码和排错经验。这套东西解决什么问题简单说就是让业务系统里那些“按流程走”的环节——比如请假审批、采购申请、合同会签——从写死的状态字段变成可动态编排的流程。审批节点加个人、条件分支怎么走不用再改代码重新发布流程引擎帮你搞定这些事。适合谁看正在做集成、或者刚接触 Activiti 7 准备选型的开发者。如果你是运维或者只想知道它能干什么也可以看前两章了解个大概。1. 方案选型与核心依赖配置1.1 为什么最终选了 Activiti 7 而不是其他引擎动笔之前先聊聊选型。工作流引擎不只是 Activiti 一家还有 Flowable、Camunda以及国内一些自研引擎。我这次选择 Activiti 7核心理由有三个。第一Activiti 7 的定位和 SpringBoot 2.x 贴合度最高模块化做得彻底不再像 5.x 时代那样把一堆东西捆在一起集成开发时的引入成本低。Activiti 7 原生就是为云原生和微服务场景设计的它把引擎拆成了 API、核心、Spring Boot Starter 等多个模块你需要什么引什么这一点对现代项目来说非常友好。第二Activiti 7 的流程模型设计器做得比较清爽不重配合 IDEA 插件画 BPMN 图挺顺手。新团队上手成本低流程图即代码改起来直观。第三Flable 和 Camunda 功能上确实更丰富但它们的社区版在某些配套组件上会做一些限制而 Activiti 完全开源社区活跃度也够中小团队用起来没有心理负担和法律风险。对比一下三个引擎的核心差异方便你选型维度ActivitiFlowableCamunda社区活跃度高高高云原生适配好好好产品化配套一般一般最完善学习资料质量零散但够用官方文档较好官方文档最完善与SpringBoot集成Starter 完善Starter 完善Starter 完善选型这块我的建议是如果项目是纯内部系统没有特别复杂的人工任务编排Activiti 7 完全够用而且轻。如果后期需要做流程引擎的对外服务化、要监控大盘、要非常庞大复杂的网关和事件机制那再去看 Camunda 也不迟。1.2 引入依赖前的版本兼容性坑点Activiti 7 的版本策略和旧版不同。Activiti 7.1.0.M6 是一个里程碑版本虽然带 M6 后缀但它被很多生产项目采用稳定性是可以的。不过这里有个很关键的点也是很多人翻车的地方就是要小心 SpringBoot 版本和 Activiti 的兼容性。先看版本对应关系。Activiti 7.1.0.M6 基于 Spring Boot 2.7.x 开发它本身是通过自动配置机制实现的所以你在pom.xml里引入activiti-spring-boot-starter后Spring Boot 会自动装配引擎相关的ProcessEngine、各 Service。但是如果你是 Spring Boot 3.x那就要注意了Activiti 7 最初的设计是跑在 Java 8/11 上的Javax 命名空间Spring Boot 3 是 Jakarta EE直接上去大概率启动报ClassNotFoundException或者各种 Bean 注入失败。我实际用的组合是这套参考性和稳定性都验证过SpringBoot 2.7.x我用的 2.7.18Activiti 7.1.0.M6mybatis-plus 3.5.3mysql-connector-java 8.0.33JDK 8 或者 11 都行这里有个额外的坑就是 Activiti 7 的 Starter 包路径或命名。你在搜资料的时候可能会看到有人用activiti-spring-boot-starter有人用flowable-spring-boot-starter这是完全不同的两个东西千万别混。Flowable 是 Activiti 的一个分支包名、API 都有差异代码抄混了基本编译不过去。核心依赖清单示例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency特别注意MyBatis Plus 或者 MyBatis 和 Activiti 的自动配置会有极小概率的冲突主要表现为 Bean 扫描时Activiti 内部的 Mapper XML 找不到或者 MyBatis 的拦截器和 Activiti 的插件逻辑互踩。这个场景下可以先不加 MyBatis Plus等 Activiti 正常跑通了再加。如果必须一起上可以参考后面第 5 章的排错配置。1.3 数据库初始化与核心表结构认知Activiti 7 启动后默认会检查并自动创建表结构。如果你不在配置里指定数据库策略它有一个默认值通常database-schema-update对应true在生产或某些配置下默认可能不是所以还是要手写。在 application.yml 中必须给的关键配置spring: datasource: url: jdbc:mysql://localhost:3306/activiti_test?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/ShanghainullCatalogMeansCurrenttrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver activiti: db-history-used: true history-level: audit database-schema-update: true async-executor-activate: false有几点要详细解释否则你会踩坑。db-history-used和history-level决定是否记录历史数据。如果不上历史记录你的流程实例执行完就彻底查不到执行轨迹了。默认是 false很多人集成完说“为啥流程实例跑完就失踪了”就是这个问题。测试阶段也建议开audit级别它能记录流程实例、任务、活动执行等关键历史。database-schema-update设成true启动时自动建表。这个在开发阶段没问题但生产环境建议改成false然后手动执行 SQL 脚本建表避免权限问题和表结构自动变更的不可控风险。async-executor-activate这个是是否开启异步执行器。如果你不开发复杂的会签、异步消息拿到流程实例已经很快了可以关掉减少线程开销。要注意的是如果用的是 Activiti 7有一些 Job 执行器的类加载路径变了开启异步执行器需要配置 Job 相关的线程池参数否则默认参数下容易出现线程池资源争抢导致流程任务不按时执行。Activiti 7 自动建表后核心表大概有以下几类通用数据表ACT_GE_BYTEARRAY二进制数据流程定义文件和图片都在这里、ACT_GE_PROPERTY引擎属性流程存储表ACT_RE_DEPLOYMENT部署记录表、ACT_RE_PROCDEF流程定义表运行时数据表ACT_RU_EXECUTION执行实例表这是最核心的表每个流程实例就在这里、ACT_RU_TASK待办任务表、ACT_RU_VARIABLE流程变量表历史数据表ACT_HI_PROCINST历史流程实例、ACT_HI_TASKINST历史任务、ACT_HI_ACTINST历史活动、ACT_HI_VARINST历史变量身份数据表ACT_ID_USER、ACT_ID_GROUP等我在第一次跑起来之后会先执行一条 SQL 看一下表数量SHOW TABLES LIKE ACT\_%;如果看到大概 20 多张表就说明引擎已经正常工作了。此时可以直接把database-schema-update改为false除非你有新版本升级需求。2. BPMN 流程文件设计与部署细节2.1 一个标准审批流的 BPMN 文件怎么设计Activiti 7 的流程定义文件依然是 BPMN 2.0 标准 XML后缀.bpmn20.xml或者.bpmn都行实际上引擎识别的是 XML 内容里的definitions标签里的targetNamespace。我建议使用.bpmn20.xml有些旧版本解析器对.bpmn扩展名偶尔不识别。为了讲清楚集成流程我设计一个典型的报销审批流程包含单节点审批、条件分支、结束三个关键要素开始 - 填写报销单用户任务 - 部门经理审批用户任务 - 会签排他网关 - 财务审批用户任务 - 结束这里面我需要解释下为什么用“排他网关”而不是“并行网关”。如果报销金额小于等于 5000只需要部门经理审批完直接结束大于 5000则继续走财务审批。排他网关就是根据一个流程变量表达式来选择单一分支符合这种“非A即B”的业务逻辑。对应的 BPMN 文件核心结构长这样?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://example.activiti.org/expense process idexpenseApproval name报销审批流程 isExecutabletrue startEvent idstartEvent name开始/ userTask idapplyTask name填写报销单 activiti:assignee${applyUser}/ userTask idmanagerApprove name部门经理审批 activiti:assignee${managerUser}/ exclusiveGateway idamountGateway name金额判断/ userTask idfinanceApprove name财务审批 activiti:assignee${financeUser}/ endEvent idendEvent name结束/ sequenceFlow idflow1 sourceRefstartEvent targetRefapplyTask/ sequenceFlow idflow2 sourceRefapplyTask targetRefmanagerApprove/ sequenceFlow idflow3 sourceRefmanagerApprove targetRefamountGateway/ sequenceFlow idflow4 sourceRefamountGateway targetReffinanceApprove conditionExpression xsi:typetFormalExpression ![CDATA[${amount 5000}]] /conditionExpression /sequenceFlow sequenceFlow idflow5 sourceRefamountGateway targetRefendEvent conditionExpression xsi:typetFormalExpression ![CDATA[${amount 5000}]] /conditionExpression /sequenceFlow sequenceFlow idflow6 sourceReffinanceApprove targetRefendEvent/ /process /definitions这里面有几个设计细节activiti:assignee是直接写死处理人。这种是简单方案实际项目往往需要动态指定所以 XML 里用的都是变量表达式比如${applyUser}启动流程实例时通过变量传入真实用户ID这一点很关键。isExecutabletrue这个属性能漏掉吗能漏掉后果就是部署成功但你启动不了流程会提示“Cannot find process definition for processId”很多人排查半天发现是这里少了标签。排他网关的默认流最好设置一个default属性这样即使金额变量没传或者为 NULL也不会把流程卡死在网关上而是走默认分支。2.2 流程文件的资源路径与自动部署机制Activiti 7 部署流程定义有多种方式自动扫描、手工部署、API 上传 BPMN。开发阶段我推荐自动扫描就是利用 Starter 自带的流程资源部署机制。默认它会扫描classpath:/processes/目录下的.bpmn20.xml和.bpmn文件。启动 SpringBoot 应用时它会自动创建部署并挂到流程定义库里。这个机制在你修改流程文件后重启时会生成新的流程部署但是版本号会递增而不影响已启动的流程实例。这是 Activiti 比较给力的地方——流程定义是“多版本生效”的旧实例继续按旧版本走新实例按新版本走。我不想用默认目录自定义路径可以通过配置activiti: process-definition-location-prefix: classpath:/processes/这个配置告诉引擎去扫描哪个目录。你可以放资源目录下的processes/文件夹。还有一个非常容易踩坑流程变量或处理人表达式里的用户在 Activiti 7 里不会自动建身份数据。也就是说如果你在 XML 里写死activiti:assigneezhangsan但ACT_ID_USER表里没有 zhangsan跑流程和任务查询是不受影响的但是在某些 API比如查询某个用户的可办任务里是查不到的。所以后来我做项目基本都是通过管理员 Service 去同步用户到身份表或者在任务创建后直接通过TaskService.setAssignee覆盖处理人。2.3 手写部署 API 的场景自动扫描适合流程定义固定且随应用启动的场景但如果是运营后台经常要上传新的流程就要用部署 API 动态操作。这里需要注入RepositoryService核心代码Service public class ProcessDeployService { Autowired private RepositoryService repositoryService; public String deploy(String bpmnFileName, MultipartFile file) throws IOException { Deployment deployment repositoryService.createDeployment() .name(bpmnFileName) .addInputStream(bpmnFileName, file.getInputStream()) .deploy(); return deployment.getId(); } }部署完成后还需要确认流程定义是否已经生效。可以通过查询流程定义列表验证ListProcessDefinition list repositoryService.createProcessDefinitionQuery() .processDefinitionKey(expenseApproval) .latestVersion() .list();这里检查有效性最好的一个指标是查询出来的记录是否包含deploymentId和version。有时部署 API 会静默失败——比如 BPMN 文件本身格式非法引擎会抛XMLException但如果你没捕获前端只看到 500很迷惑。建议无论谁调用部署都要做好异常处理并落日志内容是流程图位置和具体的解析异常。3. 核心 API 调用链路与流程操作实现3.1 启动流程实例的完整链路Activiti 7 集成后最重要的三个服务接口就是RuntimeService、TaskService、HistoryService加上RepositoryService是四大金刚。这里我用一张图说明它们的职责为了纯文字说明我用表格拆一下Service职责典型方法RepositoryService流程定义和部署管理createDeployment, createProcessDefinitionQueryRuntimeService启动流程实例、变量控制、触发信号startProcessInstanceByKey, setVariableTaskService任务查询、认领、完成、委派createTaskQuery, complete, setAssigneeHistoryService历史流程实例、任务、活动查询createHistoricProcessInstanceQuery, createHistoricActivityInstanceQuery启动流程实例是业务系统的动作触发点。假如用户提交了报销单后端就要调用 RuntimeService 开启新流程实例。我最常用的启动代码public String startExpenseProcess(String userId, BigDecimal amount) { MapString, Object variables new HashMap(); variables.put(applyUser, userId); variables.put(amount, amount); variables.put(managerUser, manager001); variables.put(financeUser, finance001); ProcessInstance processInstance runtimeService .startProcessInstanceByKey(expenseApproval, variables); return processInstance.getId(); }这段代码里有几个隐藏逻辑startProcessInstanceByKey是按流程定义 Key 启动最新版本流程实例字段是 BPMN 里process标签的id不是那个中文 name 字段。如果你传中文名启动绝对报错ResourceNotFound。传参businessKey是官方推荐的做法。业务侧自己的单据号比如报销单号一般关联到流程实例用runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey, variables)。后面通过业务单号回查流程状态是一个标准模式强烈建议使用可以避免在业务表和 Activiti 的ACT_RU_EXECUTION之间来回匹配。变量会存入ACT_RU_VARIABLE在排他网关判断金额时被使用这就是${amount 5000}表达式运行时去取变量的机制。3.2 任务查询与完成任务的权限问题流程启动后下一棒就是第一个 UserTask。谁来处理任务根据 BPMN 里定义的 assignee 变量。业务系统处理待办时最常用的方法是查询当前登录人可处理的任务列表。不同用户查询待办代码public ListTask queryTodoTasks(String userId) { return taskService.createTaskQuery() .taskAssignee(userId) .orderByTaskCreateTime() .desc() .list(); }在 Activiti 7 中taskAssignee查询的就是ACT_RU_TASK.ASSIGNEE_字段。这里有一个常见的坑如果 BPMN 里activiti:assignee${managerUser}而你启动流程时忘了放managerUser变量那么这个任务就变成了“悬空任务”没有 assignee。普通用户查询自己任务时根本看不到它只有用taskCandidateUser查询候选人或者管理员查询所有任务才能发现它。完成任务的核心操作public void completeTask(String taskId, MapString, Object variables) { taskService.complete(taskId, variables); }很多人会在这件事上掉坑完成任务时传了变量比如更新amount但 BPMN 里的排他网关条件读取的是流程变量不是本地任务变量。如果你用taskService.setVariableLocal(taskId, amount, ...)而不是在complete时传入全局变量网关判断的时候取不到新值流程就可能走错分支。还是要纠正一下complete(taskId, variables)默认是把变量放到流程实例作用域的。所以注意在完成节点时凡是网关条件需要的变量请务必随 complete 一起传全局变量确保变量作用域正确。这是一个典型的“代码没报错但流程顺序错乱”的案例。用户组任务和候选人场景complete逻辑也类似但查询方式不同。如果你的 BPMN 节点用的不是 assignee 而是候选组userTask idmanagerApprove name部门经理审批 activiti:candidateGroupsmanagers_grp/查询时就要用taskCandidateGrouptaskService.createTaskQuery().taskCandidateGroup(managers_grp).list();如果候选组有好几个可以用taskCandidateOrAssigned这个查询会匹配 “候选人或指定处理人”覆盖面广避免漏单。3.3 流程状态推进与历史记录查看的API流程走到一半业务方想知道这个报销单现在卡在谁那里、过去经过了哪些节点这就是历史查询的标准场景。用 HistoryService 查询当前流程实例的活动节点和历史活动。查询当前流程实例状态public ListHistoricActivityInstance getProcessHistory(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list(); }这条链路返回的数据是完整的活动轨迹从 StartEvent、每个 UserTask 的开始和结束时间到 EndEvent 的触发。用这些数据前端可以画出“流程图自动高亮进度”实际上很多系统就是拿着这些历史数据去和 BPMN 图里的坐标做匹配渲染出来的。查询当前活动节点就是流程现在停留的位置public ListHistoricActivityInstance getRunningActivities(String processInstanceId) { return historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .unfinished() .list(); }这个unfinished()查询很实用它会列出所有已开始但未结束的活动实例通常对应当前等待中的 UserTask 节点。对于排他网关它会显示当前活动的网关。这里有一个容易混淆的概念HistoryService查询出来的是“历史”数据但是在流程没有结束前当前活动节点也是记录在历史表里的。所以如果你要展示“当前卡在哪个节点”不要去查ACT_RU_TASK的表直接查历史活动实例的unfinished条件是最稳的方式。4. 常见问题与排错技巧实录4.1 使用 Activiti 7 时最常遇见的启动失败问题我在集成 Activiti 7 时遇到的第一个坑是启动 SpringBoot 后容器直接回滚最典型的问题是数据库连接相关的表不存在或者前缀不匹配。报错信息类似Caused by: org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Table activiti_test.ACT_GE_PROPERTY doesnt exist这个问题的原因是 Activiti 的 SQL 默认建表使用ACT_前缀如果你的数据库连接里之前残留了别的数据库或者表前缀被全局配置修改过就会找不到。解决办法是确认 datasource URL 里nullCatalogMeansCurrenttrue这一项MySQL 8.x 的小坑。没有这个参数Activiti 执行某些初始化语句时会把 catalog 识别为 NULL然后去连接别的库导致在错误的库上执行 SQL。另外Activiti 7 的建表 SQL 在低版本 MySQL5.5 及以下会执行失败因为用到了较新的数据类型。生产环境尽量用 MySQL 5.7 或 8.0。第二个典型问题——Table ACT_HI_XXX doesnt exist且 schema update 配置为 true 也无济于事。这个场景是你改了history-level之后引擎并不会自动补齐历史表需要手动清理或者插入缺表。最简单的做法是删掉全部 ACT 表重启让引擎重建或者找到对应版本的activiti.mysql.create.history.sql执行补表。4.2 MyBatis Plus 和 Activiti Bean 扫描的冲突处理很多人集成时项目里已经用了 MyBatis Plus然后引入 Activiti 后发现 Mapper 扫描异常表现为某个 Mapper 接口提示找不到 SQL或者 Activiti 的 Job 执行出错。产生这个问题的根因是两者都依赖 MyBatis且自动配置会扫描同一类路径。Activiti 内部用的 SqlSessionFactory 和 MyBatis Plus 的 SqlSessionFactory 存在相互覆盖的可能导致其中之一失效。我实测下来的稳定方案是这样配置在启动类排除 Activiti 的自动配置中的 MyBatis 相关自动装配或者反过来让 Activiti 的自动配置优先。确保自定义 Mapper 扫描包与 Activiti 内部包完全隔离不要嵌套扫描。具体代码SpringBootApplication MapperScan(basePackages com.example.mapper) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }MyBatis Plus 的插件比如分页插件和 Activiti 的自定义拦截器偶尔会冲突表现为分页查询总数错误或者 SQL 执行抛出异常。这种时候可以使用MybatisPlusInterceptor只针对业务 Mapper 使用或者将分页查询改到服务层手动实现绕过插件冲突。如果你没有 MyBatis Plus 需求就尽量不要引 MyBatis Plus直接裸 SpringBoot Activiti问题最少。4.3 流程实例启动后任务查询不到的处理有段时间我在测试环境明明启动成功了但是taskService.createTaskQuery().taskAssignee(user01)查不到任何任务。排查顺序一般是三步第一步确认节点 assignee 表达式是否解析到了。如果表达式是${managerUser}而且启动流程时变量里没有这个 Key那么引擎解析表达式失败后不会主动报错只是任务没有 assignee 而已于是这个任务就变成了“无主任务”。此时用taskService.createTaskQuery().processInstanceId(pid).list()不带任何 assignee 条件查询这个流程实例下的所有任务如果看到记录但 assignee 为空基本就是这个原因。顺带说一个技巧在没有 assignee 时快速塞一个处理人Task task taskService.createTaskQuery() .processInstanceId(processInstanceId) .singleResult(); if (task ! null task.getAssignee() null) { taskService.setAssignee(task.getId(), user01); }第二步确认流程是否卡在排他网关。如果流程卡在没有匹配条件的网关ACT_RU_EXECUTION里会有一条记录当前的 activity id 指向网关 ID但是没有对应的任务产生。原因基本是网关条件表达式涉及变量缺失或者类型不匹配。比如你传的amount是字符串 5000不是数字表达式${amount 5000}在解析时就会出错。第三步确认流程是否已经提前走到 EndEvent。如果 BPMN 条件写错了比如amount 5000分支误写为amount 5000可能用户提交了一个小额报销流程直接就结束了运营那边一无所知。历史表里 Activity 列表已经有 EndEvent但是运行时表里已经没有任何任务。但凡出现“流程启动了却查不到任务”先查历史活动再倒推运行时状态比凭感觉猜效率高很多。这里我提供了一个流程图排查顺序的思路正规的做法是统一封装一个“流程状态检查接口”输入是业务单号输出是历史轨迹、当前节点、任务处理人列表。能省掉大量沟通排查时间。4.4 流程图中文乱码的定位与实际修复Activiti 7 生成流程跟踪图的时候用到了 Java AWT 的字体渲染。很多服务器环境没有安装中文字体导致生成的图片里中文字符变成方框“口口口”这个问题在 Linux 服务器部署后尤其明显。解决办法是往服务器装入中文字体比如宋体或者文泉驿正黑。CentOS 可以简单地安装字体文件yum install -y fontconfig然后拷贝 Windows 下的simsun.ttc或者下载开源中文字体到/usr/share/fonts/目录执行fc-cache -fv刷新字体缓存。如果还是不行可以通过java.awt.Font手动注册字体但更推荐直接从服务器层面解决。字体问题不解决的话部署向导、流程图预览全是方块客户一看就会认为产品质量不行。我自己的经验是安装完字体以后重启应用再生成一次流程图就能正常渲染中文。如果你用的 Docker 镜像偏瘦还需要确认镜像里有 fontconfig 和基础字体否则同样会乱码。4.5 Activiti 7 与 SpringBoot 2.7 集成时的日志与事务测试调试工作流最烦的就是看不到 SQL 日志。Activiti 的 Mapper 也是基的 MyBatis所以你只要开启 MyBatis 的 SQL 日志就可以logging: level: org.activiti: debug org.mybatis: debug如果日志级别还嫌多更精准的方式logging.level.org.apache.ibatis: DEBUG这样就能看到 ACT_ 开头的每一条 SQL 操作。排查“变量存没存上”、“任务有没有被更新”的时候直接看 SQL 最直白。关于事务集成我建议在启动流程和完成任务的方法上统一加Transactional(rollbackFor Exception.class)。比如“提交报销单”这个动作既要生成业务单据又要启动流程实例如果业务单保存成功、流程启动失败没有事务的话表单和流程就出现脏数据。加了事务之后两边一起回滚保证数据一致性。这里特别提醒一个测试技巧不要只测“happy path”一定要测“驳回语”、“金额校验失败”、“重复提交”这些反例。工作流引擎的表现很多时候不是直接抛异常而是“流程往意想不到的分支走了”。用 JUnit 模拟不同的变量组合去跑同一个 BPMN把网关条件测试覆盖到位比什么都管用。5. 生产环境增强建议与流程性能心得5.1 用户体系对接与权限模型怎么设计Activiti 7 自带了一套简单的身份管理表结构里ACT_ID_USER、ACT_ID_GROUP就能存。但实际业务系统都有自己的用户体系根本不会用 Activiti 自带的身份表。我的做法是用户和用户组完全不在 Activiti 内部创建而是只在流程引擎里维护“逻辑用户 ID”和“逻辑组 ID”。当调用taskService.setAssignee时传入的是业务侧的用户主键candidateGroup传入的是角色编码不做同步而是让任务是“只读引用”。好处很直接不需要操心用户增删改与 Activiti 表同步问题也为后面做流程引擎服务化多个业务系统共用同一个引擎铺平了路。如果用默认的IdentityService做管理identityService.saveUser(identityService.newUser(zhangsan)); Group group identityService.newGroup(managers_grp); group.setType(role); identityService.saveGroup(group); identityService.createMembership(zhangsan, managers_grp);这个方式在原型验证阶段用用还行生产环境别这么搞老是对表同步这件事纠结纯属给自己找罪受。5.2 流程引擎多实例与性能调优Activiti 7 默认的异步执行器可以设置线程池多少个流程实例同时跑、一个实例产生多少个异步任务这些会影响数据库连接和 CPU。如果并发量上来了核心参数要调整。Async Executor 配置示例activiti: async-executor-activate: true async-executor-core-pool-size: 8 async-executor-max-pool-size: 20 async-executor-queue-size: 100同时也要给数据源连接池留够空间。Activiti 的异步任务会持续占用数据库连接连接池太小会出现拿到不到连接的报错。使用多实例或者会签节点时性能瓶颈通常在ACT_RU_EXECUTION表上的读写竞争。减少锁等待的方法是尽量把互不相关的子流程拆成独立的流程定义而不是在一个流程里塞巨多的并行网关和消息事件。我做过一个极端测试单流程 100 个顺序节点引擎能跑下来但耗时明显上升。生产环境把流程控制在 30 个以内节点、避免嵌套子流程过深对于业务团队后期的维护和流程引擎本身都会有好处。5.3 流程引擎服务化的扩展思考集成做完后有一个很自然的延伸方向把流程引擎单独拆成一个“流程中心服务”用 REST API 封装启动、办理、查询能力。前端和各个业务系统对接时不用直接依赖 Activiti 的 API而是走统一的接口层。这个做法的核心价值是隔离和收敛。流程定义文件、引擎配置、流程变量规则都沉淀在这个服务中心业务开发不需要知道 BPMN 规范也能用流程功能。同时后续升级引擎版本时只动服务中心不改各个业务系统。拆分后的接口层至少需要这些能力流程定义列表及流程图预览部署与启用/停用流程启动流程实例查询待办与已办审批同意/驳回流程变量读写流程状态历史跟踪这里需要说明一点流程引擎服务化之后原来通过线程级事务和本地 Service 调用的“同步感”会消失。调用方要做异步或补偿机制比如把“业务数据变更”和“流程实例启动”改为消息队列驱动保证最终一致性。切分方式千万不要把启动流程和处理人查询拆成了两个交互流程否则每次一次后端交互都可能因为链路变长而增加超时和后端开发难度。还有一个值得做的增强把 BPMN 流程文件的版本管理纳入代码仓库。流程定义通常不是上线了就完事了需求变更、审批规则调整都需要改流程定义。如果流程文件散落在同事电脑里改完没有版本管理上线后排查问题会非常痛苦。用 Git 管理.bpmn20.xml并让部署动作和发布流程联动回滚时整个过程都是可追溯的。6. 集成之后的运维监控经验6.1 慢流程和死流程怎么发现业务跑起来之后最怕的不是流程引擎宕机而是流程悄悄卡死。比如之前提到的网关条件不满足、assignee丢失、历史遗留的未完成任务都会让流程流转“中途失联”业务根本无感知。如果要保持流程健康后台需要建一套“僵尸流程”监控脚本。核心思路是查询所有处于运行状态的流程实例检测它的当前活动节点再对比最近的任务创建时间。超过阈值比如 7 天没有变化的就标记为异常流程。查询长时间未完成的任务Date date new Date(System.currentTimeMillis() - 7L * 24 * 60 * 60 * 1000); ListTask staleTasks taskService.createTaskQuery() .taskCreatedBefore(date) .list();这种查询在生产环境会有性能消耗建议做成定时任务配合消息通知去提醒处理人。偶尔“卡死一次”还能靠人肉排查经常出现就要审视自己的流程设计了。6.2 数据库表数据膨胀与清理策略Activiti 表会随着时间不断膨胀尤其是历史表。ACT_HI_ACTINST记录每次节点执行ACT_HI_VARINST记录每个变量快照久而久之能到几百万条。清理历史数据是必要的官方提供了 HistoryService 的删除能力historyService.deleteHistoricProcessInstance(processInstanceId);如果要做批量清理可以根据业务数据的保留周期按 createTime 范围删除历史流程。但这操作有风险删除后就没法追溯了所以生产环境建议保留最近一年或两年的数据周期。更安全和高效的方案是定期把历史数据归档到独立的历史库业务库只保留热点数据。归档任务可以用定时任务读取前一天所有已结束流程实例插入归档库再从 Activiti 业务库删除。这个流程要控制在业务低峰期避免删除大表数据时的锁表影响正常业务。6.3 不同环境使用同一套流程库的注意事项测试环境和生产环境共用同一个数据库这个错误虽然听起来低级但确实在分开部署时容易出现。我的建议是分别使用独立的 Schema 或数据库实例并在配置里明确加上不同业务的前缀。如果同一个库要放多套租户流程可以用 Activiti 自己的租户能力。部署时带上 tenantIdrepositoryService.createDeployment() .name(deploymentName) .addInputStream(fileName, file.getInputStream()) .tenantId(tenant-a) .deploy();查询流程定义时也带租户条件repositoryService.createProcessDefinitionQuery() .processDefinitionKey(expenseApproval) .tenantId(tenant-a) .latestVersion() .singleResult();这个能力对中后台的 SaaS 系统特别有用一键实现流程定义按租户隔离。激活和查询的时候带上租户条件就可以唯一要注意的是别用空租户去查有租户的流程结果一定是查不到。我自己在项目里会把租户编码设计成字符串常量在引擎调用层强制写入并且不暴露给前端调用方减少由于调用方没有传租户 ID 而导致的越权风险。Activiti 7 在 SpringBoot 项目里落地本质上就是把引擎的启动、流程部署、任务处理、历史追溯、监控排查这一整套链路跑通。如果集成顺利后端开发不用再为“状态太多、逻辑太散”头疼业务方也能自己调整审批流程。我个人在实际项目里体会最深的一点就是工作流引擎的代码量不大但流程设计、变量作用域、用户体系这几个环节每一个都值得多花时间打磨。最后再分享一个小技巧——把流程定义文件、部署脚本和业务代码放在同一个版本分支里管理发版的时候一起走线上的流程和代码永远是对得上的。