资讯动态

SpringBoot集成Activiti 7:嵌入式工作流引擎的完整落地指南

发布时间:2026/10/14 6:37:43 来源:尧图企业网站定制
如果你是从 Activiti 6 甚至更早的版本转过来的第一次在 SpringBoot 工程里引入 Activiti 7 的依赖大概率会愣一下官方 Starter 居然不再直接提供现成的 REST API 了。很多人正是因为这一点在项目一开始就选错了姿势把工作流引擎当成一个独立平台去对接结果绕了一大圈又回到 Java API 的老路上来。这篇文章我想把 SpringBoot 集成 Activiti 7 的完整过程拆开讲清楚。它面向的是那些要做审批流、任务流、工单流转但又不打算把整套引擎当作独立服务部署的团队。我会先讲清楚 Activiti 7 和旧版本的本质区别再给出一套能直接跑起来的最小骨架然后深入讲讲流程定义的部署机制、真实业务里最常见的坑以及上线前必须做的自测和调优。内容不追求面面俱到但每一步都是可以直接落地的实操经验。1. 集成前先想清楚Activiti 7 和旧版到底哪里不一样1.1 从平台到嵌入式引擎的定位转变Activiti 5/6 时代的集成方式通常是部署一个带 REST API 的引擎服务业务系统通过 HTTP 调用来发起流程、查任务、完成任务。这种方式的优点是前后端分离、语言无关但代价也很明显你要额外维护一个独立运行的服务还要考虑服务的高可用、权限隔离和接口文档。Activiti 7 在这条路上做了一个很激进的重构。它把原先的开箱即用 REST API 从引擎核心中拆了出去官方 Starter 也不再默认提供那些/runtime/process-instances、/runtime/tasks接口。7.x 的定位更像是一个可以被 SpringBoot 应用直接引用的 Java 库引擎的生命周期、事务边界、数据源都与宿主应用深度绑定。这意味着什么意味着你不能再抱着部署一个工作流平台然后调用它的老思路而是要接受我的业务应用就是引擎的宿主这个事实。审批流的发起、任务的查询和完成都通过引擎的 Java API 在应用内部完成。如果你的团队是 Java 技术栈这反而是个优势——没有跨网络的序列化开销没有接口鉴权的复杂度业务代码和流程引擎在同一个事务里。1.2 SpringBoot 版本匹配是一个硬门槛Activiti 7 对 SpringBoot 的版本要求非常苛刻。7.1.x 系列只能跑在 SpringBoot 2.x 上如果你直接把它塞进 SpringBoot 3.x 工程启动阶段就会报一堆关于javax包名不存在的错误因为 3.x 已经把命名空间换成了jakarta。我建议的组合是组件推荐版本SpringBoot2.7.18Activiti7.1.0.M6JDK1.8 或 11MySQL5.7 及以上7.1.0.M6是 7.x 里比较稳定的里程碑版本网上搜到的很多资料也都是基于它。如果你用 Maven直接在spring-boot-starter-parent父工程的依赖管理下引入即可不需要手工指定一大堆传递依赖版本。如果你确实想用 SpringBoot 3.x那就得考虑 Activiti 8/9 系列但那是另一套代码体系很多 BPMN 行为的默认值都变了迁移成本不小。我的建议是新项目如果没强约束先用 SpringBoot 2.7 Activiti 7.1 这套稳妥组合等团队对引擎足够熟悉后再考虑升级。1.3 先把数据库表结构有个整体认知Activiti 7 启动后会自动建一批ACT_前缀的表这些表按用途大致分成四类ACT_RE_*流程定义、流程模型等静态资源比如ACT_RE_PROCDEF里存的就是流程定义的元数据。ACT_RU_*运行时数据包括执行实例、任务、变量、定时器等。流程结束后这些表里的对应数据会被清理。ACT_HI_*历史数据包括流程实例历史、任务历史、活动历史、变量历史。这是你查审批记录的来源。ACT_ID_*用户和组。如果像大多数业务系统那样自己管理用户完全可以不用这些表。理解这些表的职责对排查问题很重要。比如流程都跑完了你发现ACT_RU_TASK里还有数据那大概率是流程没走到结束事件而不是引擎出 bug。再比如查历史审批记录你该查的是ACT_HI_TASKINST而不是去ACT_RU_*里翻这是新手最容易犯的错。2. 最小骨架搭建依赖、配置和引擎 Bean2.1 Maven 依赖引入先建一个空白的 SpringBoot 工程spring-boot-starter-web、spring-boot-starter-jdbc、MySQL 驱动这三个是最低限度的依赖。再引入 Activiti 的 Starterdependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependency这里有一个关键点仅仅引入activiti-spring-boot-starter还不够你还需要确保类路径里有一个可用的DataSource。Activiti 的自动配置会直接注入 Spring 容器里的DataSource如果工程里只引入了spring-boot-starter-web而没有引入spring-boot-starter-jdbc启动时会因为找不到数据源而失败。另外如果你的业务工程里同时用了 MyBatis-Plus 或其他基于 MyBatis 的框架要注意版本冲突。Activiti 7 内部依赖的是 MyBatis 3.5 系列而 MyBatis-Plus 3.x 会传递引入自己的 MyBatis 版本。实测中容易出现的现象是单独跑 Activiti 的单元测试没问题但整个应用启动时频繁报TypeException或 SQL 语句解析错误。这属于经典的版本覆盖问题解决思路是主动声明一个统一的 MyBatis 版本或者把业务工程用的 ORM 和 Activiti 的依赖做版本仲裁而不是盲目 exclude。2.2 application.yml 里的核心配置项打开application.yml先把数据源和 Activiti 的配置写好spring: datasource: url: jdbc:mysql://localhost:3306/activiti?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver activiti: database-schema-update: true db-history-used: true history-level: audit check-process-definitions: true逐项说一下含义database-schema-update: true启动时自动建表或更新表结构。开发阶段用true就行它会基于引擎维护的版本号去比对和升级不会每次启动都重新建表。生产上建议改成false由发布脚本统一控制。db-history-used: true是否启用历史表。默认其实是开启的但建议显式写出来避免不同版本默认值不一致带来的困惑。history-level: audit历史记录级别。audit会记录流程实例、任务、活动等关键信息大多数审批场景够用了。full会在audit基础上额外记录变量变更的完整快照如果要做流程画像或波次分析可以开full代价是历史表膨胀很快。check-process-definitions: true启动时扫描并自动部署 classpath 下的 BPMN 文件。这个后面会展开讲。2.3 定制引擎配置 BeanActiviti 7 的自动配置会创建一个ProcessEngine但如果你想调整一些默认行为可以通过ProcessEngineConfigurationConfigurer来插手配置过程Configuration public class ActivitiEngineConfig { Bean public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer() { return config - { config.setHistoryLevel(HistoryLevel.FULL); config.setAsyncExecutorActivate(false); config.setActivityIdFindLimit(2000); }; } }这个配置器很灵活。比如setAsyncExecutorActivate(false)是关闭异步执行器如果你的流程没有定时事件、没有消息边界事件就别让它启动那个线程池空转。某些版本下异步执行器会默认开启日志里能看到一个叫AsyncExecutor的后台线程在处理任务关掉至少能省一点无谓的线程开销。到这里一个最小骨架已经齐了。启动应用后你会看到控制台打印出一串建表日志数据库里出现ACT_RE_PROCDEF、ACT_RU_TASK等表。这就是引擎初始化成功了。3. 流程定义部署的三种方式与内部机制3.1 自动扫描部署把 BPMN 文件丢进 processes 目录Activiti 7 默认会扫描 classpath 下的processes目录凡是文件名以.bpmn20.xml或.bpmn结尾的文件都会在应用启动时被自动部署。所以你在src/main/resources/processes/里放一个请假流程的文件重启应用后就能在数据库里查到流程定义。这种方式的优点是省事缺点是容易让人忽略部署这个动作本身。很多人改完 BPMN 文件发现流程没变化就是因为只在 IDE 里修改了文件但没有重建并重启应用或者文件名没遵循标准后缀。我给个实用做法BPMN 文件名统一用流程key.bpmn20.xml这种格式比如leave.bpmn20.xml这样看文件名就能知道流程的key是什么。流程的key是 BPMN 文件里process节点的id属性比如bpmn2:process idleave name请假流程 isExecutabletrueid就是leave发起流程时用的是这个key。要注意id是大小写敏感的leave和Leave是两个完全不同的流程定义。3.2 代码部署更可控的发布方式如果你不想把部署时机交给应用启动而是希望有独立的发布入口可以用RepositoryService手动部署Autowired private RepositoryService repositoryService; public void deployProcess(String bpmnPath) { Deployment deployment repositoryService.createDeployment() .name(请假流程发布) .addClasspathResource(bpmnPath) .deploy(); log.info(部署ID: {}, 流程定义ID: {}, deployment.getId(), deployment.getKey()); }线上环境我通常建议手动部署为主先把编译好的 BPMN 文件上传到指定目录再通过运维脚本调用一个内部接口触发部署这样发布窗口可控也方便做变更记录。3.3 部署与版本为什么流程定义会有多个版本RepositoryService部署的本质是生成一条部署记录和若干条流程定义记录。当你用同一个流程key部署了新版本数据库里ACT_RE_PROCDEF表会出现两条记录版本号递增但key相同。这个机制对生产发布很关键启动流程时若不指定版本号引擎默认使用该key下版本号最大的那条流程定义而已经启动的流程实例会保留启动那一刻的版本快照不会被新版本影响。也就是说你可以安全地发布新流程版本审批中的旧流程不会突然按新流程图执行。理解这一点你就不会在发布新流程后急着去暂停旧流程定义。4. 集成路上的 5 个常见坑和排查链路4.1 版本冲突导致启动报错先从依赖树查起Activiti 7 集成的报错里最密集的一类发生在应用启动阶段典型症状是org.springframework.beans.factory.BeanCreationException Caused by: org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)这种问题绝大多数是 MyBatis 相关版本不一致造成的。排查链路是先执行mvn dependency:tree -Dincludesorg.mybatis看工程里实际生效的 MyBatis 版本有几个。如果发现项目自己的 MyBatis 版本比 Activiti 传递进来的版本低就可能出现日志能打印、但 mapper 映射文件里的动态 SQL 解析不了的情况。解决办法是在pom.xml的dependencyManagement里显式声明 MyBatis 版本让所有依赖都对齐。遇到过最隐蔽的一次是表面上没有任何冲突但业务工程里自定义了一个ObjectFactory的 Bean干扰了 Activiti 内部的 MyBatis 配置。如果你也有类似的全局 MyBatis 扩展务必检查它们是否会被同时应用到引擎内部。最省事的排查法是把业务用的 ORM 依赖临时排除看引擎单独跑是否正常逐步定位。4.2 启动后数据库里没有表多半是配置前缀写错了很多从老博客抄配置的人会写成activiti: database-schema-update: true这个配置在 Activiti 7 里是无效的正确前缀是spring.activiti。配置前缀写错引擎不会报错只是会忽略你的全部配置然后按默认值执行。默认的database-schema-update是false于是表根本不会建。排查时先确认控制台启动日志里有没有这样一行Initializing Activiti engine, version: 7.x.x.x如果连这行都没有说明引擎都没被自动配置。检查DataSource是否正确定义、activiti-spring-boot-starter是否真的在依赖里。如果日志里能看到引擎版本号但仍没建表再检查数据库账号有没有 DDL 权限以及库的字符集是不是utf8mb4。MySQL 5.7 版本下字符集不对会导致ACT_RU_TASK建表时索引长度超过限制。4.3 发起流程时报process definition not foundruntimeService.startProcessInstanceByKey(leave)报错找不到流程定义代码看着没什么问题流程文件也确实放在 processes 目录下了。这种场景我遇到太多次了。排查顺序是确认流程文件里process节点的id确实等于leave注意不要误把name属性当key用。确认文件后缀是.bpmn20.xml如果后缀写成了.xml自动扫描不会识别。确认spring.activiti.check-process-definitions没被改成false。到数据库查ACT_RE_PROCDEF看KEY_字段的实际值是多少。这里面最容易忽略的是第 4 步。有时候流程定义已经被部署过了但你改了流程图里的id数据库里就同时存在旧 key 和新 key 两条定义你代码里用的是旧 key自然报找不到。养成启动后先查一眼ACT_RE_PROCDEF的习惯能省掉一半排查时间。4.4 流程启动成功但任务查不到启动流程后代码里用taskService.createTaskQuery().taskAssignee(张三).list()查待办结果是空列表。这时候业务系统查不到任务很多人会怀疑引擎是不是坏了。任务查不到十有八九是assignee的赋值方式和你查询时的参数不匹配。BPMN 里用户任务节点常见两种写法一是写死字符串userTask idfirstTask name主管审批 activiti:assigneezhangsan /二是写表达式userTask idfirstTask name主管审批 activiti:assignee${approvalUser} /第一种写法查询时用taskAssignee(zhangsan)没毛病。第二种写法必须在发起流程时往变量里塞一个approvalUser否则该任务没有assignee用经办人维度查询自然查不到。所以查不到任务时先不要翻代码直接查ACT_RU_TASK表看ASSIGNEE_列到底存了什么值。这张表会诚实地告诉你任务的实际持有人、候选人和创建时间。另外还有个细节某些旧资料里推荐用taskCandidateUser()查候选人任务如果你用activiti:candidateUserszhangsan,lisi设置的候选人那对应用户是可以看到任务的但如果你既设了候选人又设了assignee任务会优先归属到assignee候选人查询会查不到。两者语义不同别混用。4.5 监听器里抛异常导致整个流程回滚业务流程有时候需要在流程节点进出时做一些旁路动作比如更新业务表状态、发消息通知。一群开发者习惯会直接把这些逻辑写在ExecutionListener里public class TaskCreateListener implements ExecutionListener { Override public void notify(DelegateExecution execution) { // 调外部接口发通知 notificationService.send(...); } }如果notificationService.send()抛了运行时异常你会发现整个流程实例都回滚了——发起流程的接口直接报错数据库里连流程实例记录都没留下。这是因为 Activiti 7 与 Spring 事务整合后命令执行、监听器回调都在同一个事务里任何环节抛错都会触发回滚。这里是很多人第一次体会到嵌入侵入性的地方。解决方式要看业务诉求如果你希望通知失败不影响审批流推进就要在监听器里单独捕获异常并记录重试日志或者干脆不要用监听器而是用 Spring 事件机制在引擎事务提交后再发通知。如果你希望流程状态和业务状态强一致那保持默认行为反而更合适。关键是想清楚你的业务到底容忍哪边失败。5. 和真实业务结合业务单据、待办和审批历史5.1 用 businessKey 把流程实例和业务单据绑起来工作流引擎解决的只是流程问题真正的业务数据还在你自己的业务表里。为了让流程实例能对应回一条请假单、一份报销单或一张工单最标准的做法是在启动流程时传入业务单号作为businessKeyMapString, Object variables new HashMap(); variables.put(applyUser, currentUserId); variables.put(days, 3); variables.put(reason, 项目验收请假设); ProcessInstance pi runtimeService.startProcessInstanceByKey(leave, leaveId, variables);leaveId就是业务单号可能是数据库自增主键也可能是业务系统生成的编号。同时我强烈建议在业务表里加一个process_instance_id字段把pi.getId()存进去。这样你从业务列表页进入审批详情时可以拿着业务单号迅速找到流程反向也可以从流程实例找到业务单号实现双向关联。查询时用processInstanceBusinessKey反查即可ListProcessInstance instances runtimeService.createProcessInstanceQuery() .processDefinitionKey(leave) .processInstanceBusinessKey(10086) .list();注意这里查询到的可能是运行中的流程实例。流程结束后RuntimeService里就查不到了得改用HistoryService。5.2 待办任务的查询姿势待办列表是每个审批系统最核心的页面。查某人待办的标准写法是ListTask todoList taskService.createTaskQuery() .taskAssignee(currentUserId) .active() .orderByTaskCreateTime() .desc() .listPage(0, 20);如果同一张审批单允许多人抢办则用候选人查询ListTask candidateList taskService.createTaskQuery() .taskCandidateUser(currentUserId) .list();这里的坑主要是分页。很多人图省事直接调.list()待办数量不大时无所谓但一旦某用户积压了几百条待办且流程历史数据庞大查询性能会明显劣化。引擎底层是分页查询的封装但你调.list()拿到的全量数据还是会在内存中聚合所以真正常用的还是.listPage(firstResult, maxResults)。另外查询结果里常遇到同一个流程实例产生多个并行任务的情况。如果你要做待办数量统计按实际任务数统计即可如果你要做单据数统计就需要按processInstanceId去重两个口径不一样产品需求里要提前说清楚。5.3 审批历史与流转记录的获取审批历史不能靠运行时表因为流程结束后ACT_RU_*的数据会被清掉。要查历史得走HistoryServiceListHistoricTaskInstance historyTasks historyService .createHistoricTaskInstanceQuery() .processInstanceId(piId) .orderByHistoricTaskInstanceStartTime() .asc() .list();每一条HistoricTaskInstance对应一次任务创建到完成的生命周期。如果节点被多次驳回、重新提交这里会产生多条记录你就能完整还原用户的审批轨迹。再加上HistoricActivityInstance可以查流程每一步活动的进出时间常用在对账和流程分析上ListHistoricActivityInstance activities historyService .createHistoricActivityInstanceQuery() .processInstanceId(piId) .orderByHistoricActivityInstanceStartTime() .asc() .list();这里要提个性能注意事项ACT_HI_ACTINST表是历史表里增长最快的一张因为每个节点、每个连线、每个事件都会生成一条活动记录。历史保留多久、是否需要定期归档清理是集成阶段就该和运维对齐的问题别等跑了一年才发现表已经几十个 G。5.4 用户体系对接别折腾 IdentityServiceActiviti 自带了一套用户和组管理对应ACT_ID_*表还提供了IdentityService。但真实项目里几乎没有团队会把业务用户同步到这张表里原因很简单业务系统的用户体系有组织架构、角色、数据权限不是裸的用户名和组能覆盖的。我的建议是直接绕开它。流程节点上的assignee、candidateUsers直接填你业务系统的用户 ID也就是z12345这种格式。查任务时也用这个 ID 去查。流程图里的用户表达式本质上只是一个字符串变量引擎根本不管它对应的人是否存在。如果你确实需要部门主管这种动态获取审批人的能力做法也是在发起流程时通过业务服务把主管 ID 解析出来放进流程变量approvalUser里而不是试图让引擎自己解析组织架构。这样实现简单、性能好还能复用你已有的权限数据。6. 上线前的自测脚本与性能注意事项6.1 一条完整的冒烟测试路径集成完成后先别急着开发业务功能花半天时间把引擎的基础行为摸一遍。我建议写一个集成测试类覆盖以下路径发起startProcessInstanceByKey成功返回的流程实例 ID 与ACT_RU_TASK中的任务对得上。通过taskService.complete()后流程走到下一节点ACT_RU_TASK中旧任务消失、新任务出现。驳回通过排他网关加reject变量控制走向验证任务回到上一步。重新提交再次发起或完成任务后流程能继续往后走。流程结束最后节点完成后ACT_RU_*中该流程实例的数据被清理ACT_HI_PROCINST里END_TIME_不为空。废弃流程流程定义部署后不启动检查ACT_RE_PROCDEF的版本变化是否符合预期。碰到流程结束但运行时表没清理的情况不要慌先从路由条件查起。最常见的问题是排他网关的条件表达式写错所有出口条件都不满足流程实例挂在网关上一直不结束。6.2 历史级别和异步执行器的取舍历史记录级别直接影响数据库膨胀速度和你能查询的数据粒度。我之前给的建议是audit它会在每个节点进出时记录活动历史但不会记录每次变量变更的完整快照。如果你的业务需要用户每次提交时填的表单数据快照那必须开full否则历史表单数据只能靠业务系统自己保存引擎查不到旧的变量值。异步执行器是另一个容易被忽略的性能开关。Activiti 的定时事件、异步任务、消息事件默认会走一个后台执行线程池但如果你的流程根本没有这些特性开着它纯粹浪费资源。在自定义配置里设置config.setAsyncExecutorActivate(false);能省去一堆无谓的线程空转。等真有定时事件需求了再评估是否需要单独部署一套异步执行器集群。6.3 重复提交与幂等控制最后提醒一个和引擎无关但最容易被投诉的问题任务重复提交。用户在前端把已完成按钮连点了两次或者网络超时后重试就可能导致同一个任务被complete两次。第二次操作会抛ActivitiObjectNotFoundException但用户看到的就是一个报错弹窗。处理方式有两种。前端层面在提交成功后立刻置灰按钮展示为提交中。后端层面在完成任务的接口里先做一次校验确认任务还在ACT_RU_TASK中再执行completeTask task taskService.createTaskQuery() .taskId(taskId) .taskAssignee(userId) .singleResult(); if (task null) { throw new BizException(任务不存在或已被处理); } taskService.complete(taskId, variables);这一套组合下来基本能挡住 99% 的重复提交问题。别把这种校验当成多余的生产环境里因为重复点击导致生产事故的案例多得很。工作流引擎的集成没有多高深的技术门槛真正的复杂度全在细节里。把依赖版本、事务边界、业务关联这些基础工作做扎实整套系统就能跑得稳定。如果你刚开始接入建议按这篇文章的路径先把最小骨架跑通再逐步把业务和待办界面接进来最后再回头完善用户权限和历史报表。每一步都要拿真实数据验证不要等到全部写完再联调那种方式排错成本极高。我个人的体会是Activiti 7 的嵌入式方式虽然让启动配置变复杂了一点但换来的是业务代码与流程引擎浑然一体的事务一致性。这在很多审批场景里是实打实的收益。你不需要再纠结于业务库更新成功了流程库却失败了这种跨系统一致性问题。对中等规模的 Java 业务系统来说这是一个成熟且务实的选择。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑