ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot 集成 Activiti 工作流入门:从部署到任务处理

Spring Boot 集成 Activiti 工作流入门:从部署到任务处理 简介一份面向SpringBoot开发者的Activiti工作流引擎集成学习包基于BPMN 2.0标准覆盖流程建模、部署、启动、任务处理与历史监控的完整链路适合正在做审批流、工单系统或需要快速上手Activiti的Java工程师。包体共67个文件含27个Java示例代码、13个BPMN流程模型、配套PNG预览图以及pom.xml、application.yml/properties配置、SQL初始化脚本等同时收录Activiti Designer插件及EMF相关jar包便于本地搭建建模环境。整体压缩包约23.92MB结构清晰按source资源、doc文档说明和插件区分层组织。目前已有250人学习浏览。学习后可获取可直接参考的SpringBoot集成方法、流程定义文件、数据库脚本和推荐插件版本对照文档与代码能减少自行摸索的时间快速完成工作流模块的落地与二次开发。1. 为什么是 Spring Boot Activiti 的工作流入门组合工作流引擎落到 Spring Boot 里,最常被问的不是 BPMN 图怎么画,而是“Activiti 中设计的流程怎么直接集成到应用程序中”。标题里的“文档代码”,我理解为一种能跑、能查、能改的示例工程:代码本身就是文档,依赖选择、引擎启动、流程部署、任务处理都留下可复现的痕迹,而不是散落一地的截图和配置片段。这篇按我实际搭这套组合的顺序写:先定版本和数据源,让引擎在 Spring Boot 里把表建好;再让一个请假流程从 BPMN 文件变成可查询的任务;最后把验证手段补上。主线是 Activiti 7.x 配 Spring Boot 2.x,Boot 3 的场景会给替代方案。适合刚接手工作流模块的人,也能让有经验的同事少踩两个版本和参数的坑。2. 在 Spring Boot 里装好 Activiti 引擎的三个前置决定Activiti 在 Spring Boot 里的集成方式,和 MyBatis、Redis 这类 starter 不太一样:它不只注册一个客户端,而是启动时拉起整个流程引擎,包括流程定义解析、命令执行器、历史归档和定时任务。所以装错版本或漏参数,现象往往在第一次部署流程时才暴露。先把三个前置决定做对,后面所有文档代码才有复现基础。2.1 版本对应:先锁 Spring Boot 主干,再选 Activiti 分支常见做法是直接在 Maven 里引入org.activiti:activiti-spring-boot-starter,版本跟随 Activiti 主干。以 7 系列为例,7.1.0.M6 在这套组合里用得最多,它内部对齐的是 Spring Boot 2.7.x 的自动配置机制。若工程已经升到 Spring Boot 3.x,也就是常说的“springboot 版本太高”的情况,7 系的 starter 会因为自动配置加载方式的变化启动失败,报的多是NoSuchBeanDefinitionException或ClassNotFoundException,别在这种组合上花时间排错。Spring Boot 3 场景下,我会优先看 Activiti 8 的依赖或直接评估 Flowable。Flowable 是从 Activiti 5 分叉出去的社区分支,接口风格很接近,activiti:assignee这类 BPMN 扩展大部分兼容,迁移成本主要体现在包名前缀从org.activiti换成org.flowable,这也是“springboot 整合 flowable”这类问题反复出现的原因。选择的顺序是:先确定 Boot 版本,再决定引擎分支,反过来一定会遇到兼容性补丁追不完的局面。dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter/artifactId version7.1.0.M6/version /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency这段依赖说明两点:starter 会带过来 Spring Boot 的自动配置与 Activiti 引擎包;h2是本地复现最省事的库,不需要装任何数据库就能看到建表过程。生产一般换成 MySQL 或 PostgreSQL,连接驱动和这个 starter 互不干扰。2.2 数据源与自动建表:25 张表的分工决定排查方向starter 在只有一个数据源时会直接复用 Spring 的DataSource,不需要额外声明ProcessEngineConfiguration。启动时引擎根据配置决定是否执行建表脚本,这个开关就是database-schema-update。共约 25 张表,按前缀分成五个职责域,排查问题时先看前缀再决定查哪张表。表前缀职责典型表常见误用ACT_RE_流程定义的静态资源ACT_RE_PROCDEF、ACT_RE_DEPLOYMENT删了部署记录以为能清理流程定义ACT_RU_运行中的执行与任务ACT_RU_TASK、ACT_RU_EXECUTION重启后直接清空,导致审批中断ACT_HI_已结束实例的历史归档ACT_HI_PROCINST、ACT_HI_TASKINST只知道查 RU,拿不到历史轨迹ACT_GE_字节流与通用数据ACT_GE_BYTEARRAY只翻这里找 BPMN 源文件原型ACT_ID_用户、组、关系ACT_ID_USER、ACT_ID_GROUP生产直接用内置身份表这张表在排错时有个实际用法:任务列表空了,先查ACT_RU_TASK是不是真的没有记录;如果这里有记录但接口查不到,问题在 TaskQuery 参数而不是数据;如果这里没有记录而流程实例还在,说明流程停在网关或事件上,继续往ACT_RU_EXECUTION查。ACT_HI_是历史数据大头,开了history-level: full之后,每次任务办结都会多若干行,上线前要做归档策略,否则一年后这张表会比业务表还大。2.3 一份能启动的 application.yml:四个参数说明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 history-level: full db-history-used: true check-process-definitions: true process-definition-location-prefix: classpath:/processes/database-schema-update: true表示启动时按引擎版本自动建表或升级表结构,开发环境最省事,生产环境建议改回false,由发布流程去执行升级 SQL,避免引擎版本切换时自动改表出意外。history-level: full记录完整的历史活动轨迹,排查“流程到底走了哪条线”全靠它;如果只关心结果,audit也能用,但别设成none,否则后面的回归断言全失效。check-process-definitions决定启动时是否解析并校验类路径下的所有流程文件,文件不合法会直接启动失败,这反而是好事,CI 里靠它能提前暴露 BPMN 写错的问题。最后一项是自动部署路径的前缀,默认就是它,下一章展开。MySQL 8 的驱动对useInformationSchema的处理有差异,连接串里加nullCatalogMeansCurrenttrue是常见做法,否则引擎拿不到正确的表目录,建表脚本可能落到陌生库或直接报 table not found。本地想快速起,把驱动换成 H2,URL 写成jdbc:h2:mem:activiti;DB_CLOSE_DELAY-1即可,其余配置不动。注意:定时器边界事件依赖引擎的任务执行器,7 系 starter 里spring.activiti.async-executor-activate默认不开启。流程里画了到期自动提醒这类节点,记得显式打开。3. 在 Spring Boot 里把 BPMN 部署成 Activiti 流程实例依赖和配置就位后,下一步就是把设计好的流程“塞”进引擎。很多教程到这里就一句“自动部署”,但实际工作中,自动部署的路径、重复部署的去重、发起实例时的变量,三件事分开理解才不会在报表里看到一堆重复版本。这一章用请假流程把整条链路走一遍。3.1 resources/processes 目录:自动部署的约定与修改方式starter 默认扫描类路径下的processes目录,凡是后缀是.bpmn20.xml或.bpmn的文件,启动时都会自动部署。你不需要写任何部署代码就能跑通最小案例,这也是“Activiti 中设计的流程怎么直接集成到应用程序中”最直接的答案:把设计器导出的 BPMN 文件放进src/main/resources/processes/,启动应用即可。目录前缀和文件名后缀都可以改:前缀由process-definition-location-prefix控制,后缀由process-definition-location-suffixes控制,多个后缀用逗号分隔。常见做法是保持默认,不轻易改,因为团队已经习惯从这个目录找流程文件。三种部署方式各有适用场景:部署方式触发时机适用场景注意点自动扫描Spring Boot 启动时本地开发、部署包发布文件路径或 XML 解析错误会拖垮启动代码部署运行时调用 DeploymentBuilder需要动态注册流程要做好重复部署的去重判断引擎管理接口运行时调用流程设计器后台生产环境要加权限控制自动扫描看起来简单,有个隐含风险:只要文件内容变了,重启就会生成一次新部署。同一份请假流程迭代十次,ACT_RE_DEPLOYMENT里出现十条记录是正常现象,别当故障去处理。真正需要关心的是流程定义版本,这个在 3.3 小节讲。3.2 一个能跑的最小 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 targetNamespaceLeaveProcess process idleaveProcess name请假审批流程 isExecutabletrue startEvent idstart name发起/ userTask idmanagerApprove name经理审批 activiti:candidateGroupsmanagers/ exclusiveGateway idgateway name审批结果/ endEvent idend name结束/ sequenceFlow idflow1 sourceRefstart targetRefmanagerApprove/ sequenceFlow idflow2 sourceRefmanagerApprove targetRefgateway/ sequenceFlow idpassFlow name通过 sourceRefgateway targetRefend conditionExpression xsi:typetFormalExpression![CDATA[${approved true}]]/conditionExpression /sequenceFlow sequenceFlow idrejectFlow name驳回 sourceRefgateway targetRefend conditionExpression xsi:typetFormalExpression![CDATA[${approved false}]]/conditionExpression /sequenceFlow /process /definitions这个文件是手写的最简形态:process 的id是流程定义 key,发起实例时要用;candidateGroups让任务进入“经理组”的候选池;排他网关按出线顺序逐条判断条件,表达式里的变量来自发起或办理时传入的上下文。实际设计器导出的文件会多出bpmndi图形信息和一堆扩展节点,跑引擎不需要它们,解析时也不报错,代码文档里保留核心节点就够了。条件表达式有个容易踩的写法:${approved}和${approved true}在布尔变量下等价,但如果传进来的approved是字符串true,前者会直接判为假,后者在表达式中也会得到不预期结果。所以文档代码里约定:所有布尔变量一律用 Java 的Boolean类型传递,字符串只在显示层用。3.3 部署与发起:DeploymentBuilder 和 RuntimeService 的最小代码Service public class LeaveProcessService { private final RepositoryService repositoryService; private final RuntimeService runtimeService; public LeaveProcessService(RepositoryService repositoryService, RuntimeService runtimeService) { this.repositoryService repositoryService; this.runtimeService runtimeService; } /** 手动部署一份类路径下的 BPMN,返回部署 ID */ public String deployFromClasspath(String bpmnPath) { Deployment deployment repositoryService.createDeployment() .name(文档代码示例-请假流程) .addClasspathResource(bpmnPath) .enableDuplicateFiltering() // 资源内容未变时不重复部署 .deploy(); return deployment.getId(); } /** 发起请假实例:businessKey 关联业务单号,变量进入启动上下文 */ public String startLeave(String businessKey, String applicant, String manager, int days) { MapString, Object vars new HashMap(); vars.put(applicant, applicant); vars.put(manager, manager); vars.put(days, days); vars.put(approved, false); ProcessInstance instance runtimeService .startProcessInstanceByKey(leaveProcess, businessKey, vars); return instance.getId(); } }createDeployment()返回部署构建器,addClasspathResource的路径是classpath:之后的部分,例如processes/leave.bpmn20.xml。enableDuplicateFiltering对比的是资源名和内容,内容没变就不产生新部署记录,适合代码部署方式;但注意它不能代替版本管理,流程逻辑变化时仍要主动处理新旧版本并存的问题。startProcessInstanceByKey取得是该 key 的最新已部署版本,所以线上改了流程后再次发起,新单走新逻辑;已经跑着的旧实例继续按旧定义执行,直到结束,这个行为符合大多数业务预期。businessKey建议直接放业务主键或业务单号,它在ACT_HI_PROCINST里可查,不用专门建映射表就能定位“某张请假单对应哪个流程实例”。变量vars会进入实例的启动上下文,BPMN 网关和任务监听器都能读到;把approved初始化为false是个小技巧,防止某个分支条件读到null时行为不可预期。4. Activiti 任务处理的参数边界:查询、审批、驳回流程跑起来之后,主要工作全在任务这一层:候选人怎么看到任务、审批变量怎么影响走向、驳回时怎么让任务回到正确的人手里。这三件事对应 TaskQuery 的入参、complete 的变量、网关的条件,拆开讲才不会被“任务不见了”这类问题卡住。4.1 TaskQuery 的三个身份参数:assignee、candidateUser、candidateGroup// 某人的直接待办 ListTask todo taskService.createTaskQuery() .taskAssignee(zhangsan) .processDefinitionKey(leaveProcess) .orderByTaskCreateTime().desc() .list(); // 经理组可见的候选任务 ListTask candidates taskService.createTaskQuery() .taskCandidateGroup(managers) .orderByTaskCreateTime().asc() .list(); // 签收:候选人认领后,任务归属到具体办理人 taskService.claim(candidates.get(0).getId(), zhangsan); taskService.complete(candidates.get(0).getId(), Collections.singletonMap(approved, true));三个查询参数的语义有重叠也有边界,列成表最容易看清:参数查出来的任务常见坑taskAssigneeassignee 等于该用户的任务候选任务必须 claim 后才查得到taskCandidateUser该用户属于其候选组或候选人的任务不含已认领走但没办完的taskCandidateGroup指定候选组下的任务组名拼写错误时静默返回空taskInvolvedUser参与过该实例的用户容易和 assignee 混用,语义不同代码里的claim是把候选任务变成个人任务的动作,之后taskAssignee才能命中该用户。实际产品里常见做法是:候选人点“办理”时自动 claim,或者列表页直接显示候选任务并允许抢办,两种交互对应不同参数组合,前端展示层要区分开。4.2 审批通过与驳回:complete 变量进入网关的求值顺序public void approve(String taskId, boolean approved, String comment) { MapString, Object vars new HashMap(); vars.put(approved, approved); vars.put(comment, comment); taskService.addComment(taskId, null, comment); // 审批意见写入历史表 taskService.complete(taskId, vars); // 变量先进入上下文,再触发网关判断 }complete是事务性命令:提交的变量会先写进执行上下文,再触发当前节点之后的流转。排他网关从第一条出线开始逐条求值,命中即走,没命中就继续下一条。这就是为什么案例里把approved false显式写出:只有两条出线时,第二条会被兜住,但一旦后续加了一条“转办”出线而忘记调条件,业务就会报没有匹配的出线。建议给网关配一条default出线兜底;文档代码里把出线顺序写成“先驳回后通过”,理解上更贴近审批常规。addComment的第二个参数传null,表示这是任务级意见而非流程实例级;意见本身进ACT_HI_COMMENT,办结后也能查询,别把意见塞进业务变量表。办理驳回时,如果希望任务回到上一节点而不是直接结束,需要给每个 userTask 补相应的回退流转和变量标记,例如传approvedfalse时走回退线并重新设置 assignee,这部分逻辑在网关上用表达式处理,也可以用监听器统一实现。4.3 卡在网关的报错:查 Execution 而不是猜 BPMN// 定位流程实例当前停在哪 ListExecution executions runtimeService.createExecutionQuery() .processInstanceId(instanceId) .list(); for (Execution e : executions) { // activityId 为 null 的是根执行,不为空的才是当前等待节点 if (e.getActivityId() ! null) { System.out.println(停留节点: e.getActivityId()); } }网关没有匹配出线时,complete会抛异常,很多人第一反应是去改 BPMN 重发,但更快的路径是先查ACT_RU_EXECUTION:等在那里的执行实例会告诉你它停在哪一步。结合 2.2 的表,RU_EXECUTION中有记录而RU_TASK为空,基本可以断定问题出在网关条件或事件上,而不是用户身份。另一个容易忽略的性质是事务回滚:complete内部是引擎命令,任何一条路径抛错,整个命令回滚,任务、变量、评论都不会留下半截状态。所以“审批失败但任务不见了”在原生 Api 里不会发生,真出现这种想象,先查是不是有自定义监听器在命令边界外做了额外写入。5. 用 SpringBootTest 验证 Activiti 文档代码的三条路径5.1 测试基类:部署、发起、办结一条命令跑完SpringBootTest class LeaveProcessTest { Autowired RuntimeService runtimeService; Autowired TaskService taskService; Autowired HistoryService historyService; Test void passPathShouldEnd() { ProcessInstance pi runtimeService.startProcessInstanceByKey( leaveProcess, BIZ-001, Collections.singletonMap(approved, false)); Task task taskService.createTaskQuery() .processInstanceId(pi.getId()).singleResult(); taskService.complete(task.getId(), Collections.singletonMap(approved, true)); assertEquals(1L, historyService.createHistoricProcessInstanceQuery() .processInstanceId(pi.getId()).finished().count()); } }这个测试复用了 starter 的自动部署机制:SpringBootTest一启动,processes/下的 BPMN 就已注册进引擎,所以它同时覆盖了“部署、发起、办结”三段链路。用历史表finished().count()做断言而不是查运行表,是因为它直接证明流程走到了结束节点,不受运行时残留数据干扰。失败时把org.activiti.engine日志调到 DEBUG 重跑,最有用的三个输出位置是:启动时的建表与 schema 版本、部署时的 resource registered、办结时的任务命令轨迹。最后补一个把验证固化的技巧:断言别只停留在“能办结”,再用HistoryService取这个实例的历史活动列表,按开始时间升序,断言第一个活动是start、最后一个是end。这样每次 CI 跑测试,既验证了“能发起”,也验证了“能走完”;以后修改 BPMN 或升级 Activiti 版本,流程有没有被改坏,两条断言就能看出来。把这里的流程 key 和任务路径替换成你们自己的业务,这份 Activiti 文档代码就真正集成进了 Spring Boot 工程。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进