☰
SpringBoot集成Activiti 7工作流引擎实战:从环境搭建到避坑指南
2026/10/1 18:55:43 网站建设 项目流程

做企业级应用开发久了,你早晚会遇到这样一个需求:审批流。从OA里的请假审批、报销审批,到电商后台的订单审核、客服工单流转,几乎每个系统里都藏着一套“带流程的表”。一开始我都是自己写状态机硬扛,后来流程多了、分支多了、审批角色多了,那套状态机代码就变成了谁都不想碰的“屎山”。直到我从 SpringBoot 项目里引入 Activiti 7 工作流引擎,把流程抽象成 BPMN 定义文件,才算真正从增删改查的泥潭里爬出来。

这篇文章就围绕 SpringBoot 集成 Activiti 7 这条主线,把选型思路、环境搭建、第一个可运行流程、核心 Service API、常见坑位全部过一遍。篇幅不短,但我尽量说人话,让你看完能直接在自己的项目里跑起来。适合人群是:已经会 SpringBoot 基本开发、对“工作流”只有一个模糊概念、想在一个下午之内把 Activiti 7 用起来的后端同学。

1. 选型思路:SpringBoot 项目中为什么需要工作流引擎

1.1 没有工作流引擎时的“土办法”

很多小团队接到审批需求,第一反应是在业务表上加一个status字段:0 待提交、1 待主管审批、2 待经理审批、3 通过、4 驳回。代码写起来也很快,if (status == 1) { 主管审批逻辑 } else if (...) { 经理审批逻辑 },看起来贼清晰。

但业务一复杂就完蛋。比如请假超过 3 天要走经理审批,超过 7 天还要走人事备案;比如报销金额大于 5000 要总监审批,还会签财务和法务;再比如审批人驳回后要跳回上上一个节点重新填写。这些规则一旦叠加,你写在业务代码里的“状态机”就会膨胀成一大坨难以理解的 if/else 和 switch/case。我自己踩过的坑是:改一个审批层级,要牵连十几个接口和表结构,测试的时候漏一个分支就出事故。

工作流引擎解决的核心问题,就是把“流程怎么走”从业务代码里剥离出来,变成一份独立的、可维护的流程定义文件。流程引擎负责推进节点、记录历史、分发任务,业务代码只关心“当前任务是谁的、数据是什么、办完了往下怎么走”。这本质上是一种关注点分离,和 SpringBoot 里把 Controller/Service/Mapper 分层是同一个思想。

1.2 Activiti 7 的定位与核心竞争力

Activiti 是一个用 Java 写的、遵循 BPMN 2.0 规范的开源工作流引擎。它的历史很长,早期基于 jBPM,后面又衍生出 Flowable 和 Camunda 这些分支。选择 Activiti 7 而不是自己维护状态机,是因为它自带几个很实在的能力:

  • 可视化流程定义:流程可以用图形化工具绘制,导出 XML 放到项目里,非开发人员也能大致看懂审批路径。
  • 表结构自动管理:引擎启动时会自动创建几十张ACT_开头的表,流程定义、流程实例、任务、历史、变量全都有现成模型。
  • 流程实例与业务解耦:引擎只负责流程推进,业务数据存在你自己的表里,通过businessKey关联即可。
  • 内置丰富的任务策略:单人办理、候选人组、会签、或签、驳回、加签这些复杂语义都有 API 支撑。

Activiti 7 对应的 SpringBoot 集成方式已经很成熟了,引入官方 starter 后,基本上只需要配置数据源和少量spring.activiti.*参数就能跑起来。它内部帮你完成了ProcessEngine的创建、核心 Service 的注册、配置文件的加载,开箱即用的体验在同类引擎里算是做得不错的。

1.3 Activiti、Flowable、Camunda 怎么选

如果你在做技术选型,一定会纠结这几个名字。说个真实感受:Flowable 是从 Activiti 5/6 的代码 fork 出去的,社区维护节奏更稳,升级路线清晰,很多人说它是“业内更稳定的选择”;Camunda 也源自 Activiti 早期代码,但后来另起炉灶走了轻量、微服务友好的路线,运维界面好评很多;而 Activiti 7 则是官方分支重新聚焦云原生方向,强调 SpringBoot 集成和轻量化嵌入。

我的建议比较简单:如果团队没有历史包袱,单纯想快速在 SpringBoot 里把审批流跑起来,Activiti 7 够用;如果你要长期维护、需要更保守的版本升级策略,Flowable 会更省心;如果你们团队已经熟练 BPMN 绘制,更看重管理和监控界面,Camunda 也值得试。文章后面所有内容都基于 Activiti 7,但你如果换成 Flowable 或 Camunda,核心概念是大同小异的。

2. 环境准备:SpringBoot 2.7 + Activiti 7 集成

2.1 依赖引入与版本选择

在 SpringBoot 项目里集成 Activiti 7,最稳妥的方式是用官方 starter。我的工程用 SpringBoot 2.7.18,Java 8,Activiti 版本选择7.1.0.M6。注意这是一个里程碑版本,不是最终 release,但实际项目中用得非常普遍,稳定性在社区已经得到了验证。

<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency>

如果数据库用 MySQL,记得加对应驱动;如果用 H2 做本地演示,也需要把 H2 依赖带上。这里有个容易被忽略的点:Activiti 7 的启动器一般会传递引入spring-boot-starter-jdbc,但你自己的项目最好显式确认有 JDBC 相关依赖。另外,如果使用 JDK 11 及以上版本,很可能需要额外引入 JAXB API 依赖,否则启动时会遇到缺失类的问题:

<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency>

之所以要加 JAXB,是因为 Activiti 内部解析 BPMN XML 时用到了 JAXB,而 JDK 11 开始默认不包含 Java EE 模块了。这个问题我最初能被卡十分钟,后来直接把依赖加进去才解决。

2.2 核心配置项逐个说

resources目录下的application.yml是集成第一现场。我一般这样配置:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/activiti_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: root activiti: database-schema-update: true db-history-used: true history-level: full check-process-definitions: true process-definition-location-prefix: classpath*:/processes/ async-executor-activate: false

逐个解释这些配置的作用,因为这些配置踩坑概率最高:

database-schema-update是自动建表和更新表结构的开关。有两个常用的值:true表示每次启动时检查并更新表结构,false表示不会自动变更数据库结构。第一次接入时建议设置为true,让引擎帮你建好整套ACT_表;部署到生产环境后建议改成false,把表结构的变更纳入规范化的数据库变更流程。还有一个值是drop-create,每次启动都删表重建,这个我只在本地测试用,生产环境永远不要碰,否则你的历史流程数据直接清零。

db-history-used和history-level共同控制历史数据的留存级别。历史级别有none、activity、audit、full四个档位:none不记录任何历史,activity只记录流程实例和活动节点,audit在 activity 基础上增加了变量记录,full把更细的执行细节也记下来。绝大多数业务系统用到audit就够了,我习惯直接配full,方便后续做流程轨迹回放和性能排查,代价只是历史表的膨胀速度快一些。

check-process-definitions和process-definition-location-prefix决定启动时是否自动扫描并部署流程定义文件。配置为true后,Activiti 会在classpath*:/processes/目录下找.bpmn20.xml或.bpmn文件,自动完成部署。如果你希望流程定义通过管理后台动态上传,可以关掉自动扫描,改用手动部署接口。

async-executor-activate涉及引擎内部的异步任务执行器。简单需求下把它设为false,不需要额外线程池去跑定时任务;如果流程里大量使用异步延续、定时边界事件,再开启异步执行器也不迟。这个问题属于“基础配置可跑,定时任务再研究”的取舍。

2.3 数据库准备与初始化

在集成前,我先在 MySQL 里创建一个空数据库activiti_demo,不需要手动建任何表。首次启动应用时,Activiti 7 会自动执行自己的建表脚本,创建出以ACT_为前缀的几十张表,最核心的有:

  • ACT_GE_*:通用数据表,比如属性表ACT_GE_PROPERTY,引擎版本信息就存在这里。
  • ACT_RE_*:流程定义和流程部署相关的资源表,例如ACT_RE_PROCESSDEF存流程定义,ACT_RE_DEPLOYMENT存部署包。
  • ACT_RU_*:运行时表,例如ACT_RU_EXECUTION存流程实例执行信息,ACT_RU_TASK存当前待办任务,ACT_RU_VARIABLE存流程变量。
  • ACT_HI_*:历史表,例如ACT_HI_PROCINST、ACT_HI_TASKINST、ACT_HI_ACTINST。

看到这么多表不用慌,日常开发涉及的操作基本都通过 Service API 完成,很少需要直接 SQL 操作表。但理解表结构有助于排查问题,比如待办任务查不到,大概率先去ACT_RU_TASK里看一眼当前有没有assignee_对应的记录。

2.4 自动装配原理:Activiti 是怎么被 SpringBoot 拉起来的

很多人引入依赖后,会好奇为什么项目里什么都没写,ProcessEngine就已经能注入了。这靠的是 SpringBoot 的自动装配机制。activiti-spring-boot-starter里有一个AutoConfiguration,它会在应用启动时判断当前 classpath 是否存在 Activiti 相关类,如果存在就自动创建一批 Bean。

最关键的是ProcessEngine的创建过程:自动配置类会拿到 Spring 容器里的DataSource,把它交给SpringProcessEngineConfiguration,由后者初始化流程引擎。这就意味着,如果你的项目里有多个数据源,一定要通过@Primary明确告诉 Activiti 用哪一个数据源。默认情况下 Activiti 会找容器中唯一的DataSource,多数据源时如果不做任何标记,很可能注入到错误的数据源上,启动时连ACT_GE_PROPERTY都查不到。

顺带提一句,SpringBoot 2.x 默认使用 CGLIB 代理,proxyTargetClass默认开启。这对 Activiti 的影响不大,但如果你自己写的 AOP 切面去拦截 Activiti Service 或者流程引擎相关 Bean,要注意目标类不能用 final 修饰,否则 CGLIB 代理会失败。我见到过有人给ProcessEngine加了自定义切面,结果启动报 class 无法继承的错误,就是没留神这个细节。

3. 第一个可运行的流程:请假审批全流程实操

3.1 需求案例与 BPMN 设计思路

为了不抽象讲概念,我用一个非常经典的“请假审批”流程来演示:员工提交请假申请,如果请假天数小于等于 3 天,直接主管审批即可;如果大于 3 天,除了主管审批还要部门经理审批。审批通过则流程结束,审批驳回则直接结束,状态为已驳回。

流程建模时,我会在 IDEA 里用插件绘制流程定义,或者用基于 Web 的 BPMN 设计器。绘制完成后导出 XML,放到resources/processes/leave.bpmn20.xml。当然,手写 XML 也能跑,但图形化设计有利于维护。绘制过程中有几个点要特别留意:

  • 流程的id是引擎识别流程定义的关键,后续startProcessInstanceByKey用的就是它,建议用有意义的英文名,比如leaveProcess。
  • isExecutable属性必须为true,否则该流程定义不会被当作可执行流程部署。Activiti 自动扫描时不会校验这个属性是否合法,但如果你从建模工具导出的 XML 恰好没有这个标记,流程启动时会报“process definition not found”。
  • 审批人节点使用UserTask,并在activiti:assignee中指定任务办理人。这里可以写死一个用户名,也可以不指定,运行时通过TaskService设置候选人或者指定 assignee。

3.2 编写请假 BPMN 文件的关键片段

手工写一个完整的 BPMN 文件很长,我这里是核心结构片段:

<process id="leaveProcess" name="leaveProcess" isExecutable="true"> <startEvent id="startEvent" name="开始"/> <userTask id="applyTask" name="提交请假申请" activiti:assignee="${applyUser}"/> <userTask id="managerTask" name="主管审批" activiti:assignee="${managerUser}"/> <exclusiveGateway id="gateway1" name="天数判断"/> <userTask id="directorTask" name="部门经理审批" activiti:assignee="${directorUser}"/> <endEvent id="endEvent" name="结束"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="applyTask"/> <sequenceFlow id="flow2" sourceRef="applyTask" targetRef="managerTask"/> <sequenceFlow id="flow3" sourceRef="managerTask" targetRef="gateway1"/> <sequenceFlow id="flow4" sourceRef="gateway1" targetRef="directorTask"> <conditionExpression xsi:type="tFormalExpression">${days > 3}</conditionExpression> </sequenceFlow> <sequenceFlow id="flow5" sourceRef="gateway1" targetRef="endEvent"> <conditionExpression xsi:type="tFormalExpression">${days <= 3}</conditionExpression> </sequenceFlow> <sequenceFlow id="flow6" sourceRef="directorTask" targetRef="endEvent"/> </process>

这里最核心的语法是<conditionExpression>${days > 3}</conditionExpression>,它对应的就是排他网关上的分支条件。条件表达式基于 UEL(Unified Expression Language),days是流程变量,在启动流程或者办理任务时通过变量传入。注意条件里的变量名必须和传入的流程变量 key 完全一致,否则表达式会拿不到值,网关会走默认出口或者报错。

3.3 部署流程定义:自动扫描与手动部署

流程文件放到resources/processes/目录后,只要check-process-definitions配置为true,应用启动时就会自动部署。每次启动时引擎会比较文件的 MD5 指纹,如果同一份流程定义没有变化,就不会重复生成新的部署记录。这个机制很方便,但你要修改流程内容后,重启应用才会生效;制定好的流程就不能随便改定义,否则已经在跑的历史实例还是按照旧版逻辑走。

如果需要动态发布流程,可以在后端写手动部署接口。我自制管理后台时常用的代码是:

@Autowired private RepositoryService repositoryService; public void deployProcess(String bpmnFilePath) { InputStream inputStream = new FileInputStream(bpmnFilePath); Deployment deployment = repositoryService.createDeployment() .name("动态部署-" + System.currentTimeMillis()) .addInputStream("leave.bpmn20.xml", inputStream) .deploy(); System.out.println("部署成功,ID: " + deployment.getId()); }

手动部署的好处是流程文件不用和应用打包在一起,随时通过页面或接口上传新版本的流程定义。坏处是你得自己处理版本控制和生效时间。大多数系统在起步阶段用自动扫描就足够了,我个人的习惯是把自动扫描关闭或控制好目录,只用手动接口来管理生产环境的流程变更。

3.4 发起流程实例与任务查询

流程部署完成后,业务发起端调用RuntimeService启动流程实例。以一个请假单为例:

@Autowired private RuntimeService runtimeService; public void startLeaveProcess(String employeeName, int days) { Map<String, Object> variables = new HashMap<>(); variables.put("applyUser", employeeName); variables.put("managerUser", "zhangsan"); variables.put("directorUser", "lisi"); variables.put("days", days); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leaveProcess", variables); System.out.println("流程实例ID: " + processInstance.getId()); }

启动后,当前任务会落在applyTask这个 userTask 上。员工提交申请这个动作,如果建模时activiti:assignee="${applyUser}",那么applyUser对应的人就能在待办里查到任务。

查询待办任务的代码:

@Autowired private TaskService taskService; public List<Task> queryTodoTasks(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .orderByTaskCreateTime() .desc() .list(); }

这里有个非常常见的坑:如果你只查taskAssignee("zhangsan"),那候选人任务、组任务都不会被查出来。后面要处理复杂的审批角色,还得学习taskCandidateUser、taskCandidateGroup这些查询条件。刚开始实现时不要把所有权限模型都硬塞给 assignee,否则后面扩展会很难受。

员工填写完请假申请,调用TaskService完成这个节点。大多数业务系统会在完成任务时把表单数据保存到业务表里,同时把必要的数据塞进流程变量:

@Autowired private TaskService taskService; public void completeApplyTask(String taskId, int days, String reason, boolean duplicate) { Map<String, Object> variables = new HashMap<>(); variables.put("days", days); variables.put("reason", reason); variables.put("duplicate", duplicate); taskService.complete(taskId, variables); }

任务完成后,流程沿着flow2走到managerTask主管审批节点。主管进入待办列表看到这张单,可以同意或驳回。同意就调用一次complete,引擎自动走到网关gateway1,根据days变量判断是直接结束还是进入部门经理审批。

3.5 完成任务:网关条件变量是“活水”

注意一个细节:网关判断的条件变量来自流程变量,而流程变量是可以在流程执行过程中随时更新的。所以如果员工申请时传了days=5,主管审批时又把days改成了 2,网关走向就会发生变化。这是工作流引擎非常灵活的地方,也是业务设计时需要警惕的地方:流程变量被谁改了、什么时候改的,要心中有数。

排他网关exclusiveGateway的匹配逻辑是“从上到下找第一个条件为 true 的出口”。如果所有出口条件都不满足,又没有设置默认出口default,流程执行会抛异常。实际项目中,我强烈建议给每个排他网关都配一个default出口,这样即使变量缺失或条件写错,流程也能正常往下走而不是卡死。

如果需求里要“多人会签”,比如“部门经理和人事同时审批”,只用单一 userTask 加 assignee 是不够的。Activiti 的实现方式是配置activiti:multiInstanceLoopCharacteristics,通过sequential决定是串行会签还是并行会签,再配合activiti:collection和activiti:elementVariable来遍历审批人集合。这块一开始接触会有点绕,但理解了以后会发现它在“多人审批”“比例通过”这类场景下特别能打。要注意的是,会签任务完成时,引擎会动态生成多个任务实例,ACT_RU_TASK中的IS_COUNT_ENABLED_和TASK_DEF_KEY_就是用来标识这类任务实例的。

4. 流程推进的核心 Service API 详解

4.1 RepositoryService:流程定义的生命周期管理

RepositoryService在七个核心 Service 里分管“静态资源”。流程部署、流程定义查询、流程定义挂起/激活、删除部署记录都走这个 Service。

常用操作我整理成一份笔记:

  • 部署:repositoryService.createDeployment().addBytes(...).addInputStream(...).deploy()。
  • 查询流程定义:repositoryService.createProcessDefinitionQuery().processDefinitionKey("leaveProcess").latestVersion().singleResult()。注意一个 key 会对应多个版本,latestVersion()只拿最新版。
  • 挂起/激活:repositoryService.suspendProcessDefinitionById(definitionId)/activateProcessDefinitionById(...)。挂起后,这个流程定义不能再发起新流程实例,但已存在的实例不受影响。
  • 删除部署:repositoryService.deleteDeployment(deploymentId)。这里有个坑,如果部署下已经有流程实例或者历史记录,普通的 delete 会抛异常,需要用deleteDeployment(id, true)执行级联删除。级联删除很暴力,会把这个部署下的所有历史流程记录全部清掉,生产环境慎用。

在线演示环境里我最常翻车的就是“部署成功后却启动不了流程”,原因基本是版本没查对,或者流程定义处于挂起状态。排查时可以写一个简单的查询接口把流程定义列表拉出来看一眼,SUSPENSION_STATE_为 2 就是挂起状态。

4.2 RuntimeService:流程实例的启动与状态控制

RuntimeService管“运行中的动态数据”。流程启动、信号事件、执行实例查询、流程变量增删改查都在这里。和RepositoryService最大的区别是:前者管定义,后者管实例。

启动流程有三种常见方式:

  • startProcessInstanceById(processDefinitionId):按定义 ID 启动,每次都要先查出来定义 ID。
  • startProcessInstanceByKey(processDefinitionKey):按定义 key 启动,默认启动最新版本,业务系统最常用。
  • startProcessInstanceByKeyAndBusinessKey(key, businessKey):额外把业务主键关联进去,这个业务主键可以是请假单 ID、订单 ID、工单 ID。有了 businessKey 之后,HistoryService查询历史流程时可以像查业务表一样方便。
ProcessInstance processInstance = runtimeService.startProcessInstanceByKeyAndBusinessKey( "leaveProcess", "LEAVE-2025-0001", variables );

流程变量这块,RuntimeService提供setVariable(executionId, key, value)、setVariables(executionId, map)、getVariable(executionId, key)。同一个流程实例下,不同执行节点上的变量作用域不同,涉及并行分支时尤其要小心,我在实际项目里因为变量作用域问题吃过亏,后面会专门提一下。

4.3 TaskService:待办任务的查询与办理

TaskService是实际业务代码里调用频率最高的接口。只要登录用户要对审批单做操作,底层一定跑的是 TaskService。核心能力可以分三类:

任务查询:createTaskQuery()可以组合出大量条件。除了taskAssignee,还有taskCandidateUser(候选人中包含当前用户)、taskCandidateGroup(候选组中包含当前组)、processDefinitionKey、taskCreatedAfter、taskCreatedBefore等。查询条件越精准,SQL 定位越高效,也越不容易查错数据。

任务办理:complete是完成任务的主入口,有complete(taskId)和complete(taskId, variables)两种重载。每次 complete 都是引擎状态机的关键转折点,流程会立即推进到下一个节点。如果下一个节点依赖缺失变量,这里就可能抛异常。

任务分配:setAssignee(taskId, userId)把任务指派给某个具体用户;addCandidateUser把用户加入候选人名单;claim表示认领任务,认领后 assignee 变更为当前人;unclaim撤销认领,把 assignee 清空。市面上大部分 OA 系统的“取回”“转办”“委派”功能,底层基本都是这些方法组合出来的。

我一开始接工作流的时候,特别喜欢在业务表里搞一堆抄送记录,后来发现直接复用 TaskService 的候选人机制就特别舒服:抄送给谁,就把谁加到 candidateUser 里,对方按taskCandidateUser查就能看到。

4.4 HistoryService:历史记录与流程追溯

如果说前三类是“现在时”,HistoryService就是“过去时”。它的核心价值有两个:一是做“已办列表”和“流程轨迹回放”,二是做流程分析。

已办列表要从历史任务表里查:

@Autowired private HistoryService historyService; public List<HistoricTaskInstance> queryDoneTasks(String assignee) { return historyService.createHistoricTaskInstanceQuery() .taskAssignee(assignee) .finished() .orderByHistoricTaskInstanceEndTime() .desc() .list(); }

流程轨迹回放,则是把流程实例经过的所有活动节点按时间捞出来:

List<HistoricActivityInstance> activities = historyService .createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .orderByHistoricActivityInstanceStartTime() .asc() .list();

有了这个列表,前端就能画出类似“已走路径”的时序图。很多项目还要在流程图高亮显示当前节点,用repositoryService.getProcessDiagram配合historyService.createHistoricActivityInstanceQuery().finished()判断哪些节点已经完成,这算是一个标准的扩展点。注意流程图片生成时,如果节点名称涉及中文,在 Linux 服务器上很容易出现豆腐块乱码,需要设置流程图字体,我之前在项目里吃过这个亏。

4.5 其他 Service 的补充说明

  • IdentityService:管理用户、组以及用户和组之间的关系。在实际企业系统中,用户体系通常在业务系统里已经建好,很多人干脆不通过 IdentityService 管理用户,而是直接使用用户 ID 字符串作为 assignee。这样集成成本低,但流程引擎内部不会感知这些用户是否真实存在。
  • ManagementService:主要用于引擎运维,比如执行数据库命令、查询引擎属性。这类操作很少出现在业务代码里。
  • FormService:管理流程表单数据。如果项目前后端分离、表单完全由前端控制,FormService 用得不多;如果依赖 Activiti 自身的表单引擎,则需要在流程定义中配置 formKey。
  • DynamicBpmnService:7.x 提供,可以动态修改流程定义中的节点信息,比如动态改审批人、修改节点名称,在个性化需求重的系统里是宝贵的工具箱。

5. 常见问题与避坑指南

5.1 启动失败:DataSource 循环依赖与自动配置覆盖

我见过最多的集成问题都出在多数据源场景。用户引入了 Activiti 依赖后,项目里本来已经有一个主数据源和几个业务数据源,没有做任何标记。启动过程中 Activiti 试图自动注入一个DataSource,发现容器里有多个候选 Bean,直接报NoUniqueBeanDefinitionException。

解决方式也很简单:在主数据源类上加上@Primary,让 Activiti 明确选择它。

@Bean @Primary @ConfigurationProperties(prefix = "spring.datasource") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); }

另一个相关问题:SpringBoot 2.6 之后默认禁止循环依赖,而某些版本的 Activiti 自动配置和用户自定义配置之间存在循环引用,导致启动报The dependencies of some of the beans in the application context form a cycle。遇到这种提示,先排查是否真的是自己的 Bean 设计有问题。如果确定没问题只是第三方配置需要,可以在配置里设置spring.main.allow-circular-references=true,但我不太建议用这个开关来掩盖问题,根治还是要看清依赖图。

5.2 流程图片显示乱码与流程文件编码

部署到服务器后,用户查看流程图发现所有中文节点都显示成方框,这是典型的 Linux 服务器字体缺失或 Java 图形环境字体不正确引起的。基本解法是在生成流程图的时候显式指定字体名称,同时确保服务器上有对应的中文字体,比如宋体或WenQuanYi。

ProcessDiagramGenerator generator = processEngine.getProcessEngineConfiguration() .getProcessDiagramGenerator(); InputStream diagram = generator.generateDiagram( bpmnModel, "png", activityIds, flowIds, "宋体", "宋体", "宋体" );

generateDiagram里传的三个字体参数分别对应活动节点字体、连接线字体、标注字体。如果你本地用 Windows 开发时不指定字体也能正常显示,一上 CentOS 服务就乱码,就是没传字体的锅。

流程文件的编码也容易踩坑:processes目录下的 BPMN 文件如果包含中文注释,必须保证文件是 UTF-8 编码,否则解析阶段可能出现乱码导致 XML 解析失败,启动时直接报错。IDEA 里一般默认 UTF-8,但从同事电脑拷贝过来的文件有可能被改过编码,遇到异常先看文件实际编码。

5.3 流程变量作用域:本地变量还是全局变量

在并行网关场景中,这个问题几乎一定会出现。比如流程在并行分支中分别执行 A 节点和 B 节点,A 分支设置了一个变量,B 分支却读不到,查ACT_RU_VARIABLE发现变量在某个 execution 上,不在根执行上。

Activiti 7 支持局部变量(Local Variable)和全局变量(Global Variable)。setVariable默认是设置当前执行实例的变量,如果这个执行实例不是根执行实例,那其他分支无法观察到;setVariableLocal则显式只设置在本地执行实例上。反过来,往根执行实例设置变量,所有分支都能看到。

一个保险做法是:流程变量统一通过runtimeService.setVariable(processInstanceId, key, value)设置,通过流程实例 ID 去设置会落在根执行实例上,这样全局都能读到。在网关条件判断时,所有分支读取到的都是这个根实例范围内的变量,不容易出现变量丢失问题。

5.4 历史级别太低导致无法排查问题

在一个快速迭代的项目里,我想节省数据库空间,把history-level配置成了none。结果后来一个流程实例走到网关后丢失,问题怎么也查不到,因为历史表全是空的。想回滚一个错误操作的流程实例,也没有任何记录可供参考。

从那以后我再也不用none。建议最低用activity,但业务系统至少用audit。因为audit会保留流程变量历史,当你需要复盘某个审批单当时传入的参数是什么时,直接查历史变量表就能还原现场。生产环境如果担心数据膨胀,可以做定期归档和清理,不要以牺牲可追溯性为代价。

5.5 MySQL 8 驱动与建表大小写敏感性

当前很多项目已经切到 MySQL 8,如果使用com.mysql.jdbc.Driver这种老驱动,启动时大概率报Loading class 'com.mysql.jdbc.Driver' is deprecated或者连不上。要把驱动配置改成com.mysql.cj.jdbc.Driver,同时在 JDBC URL 中加上serverTimezone=Asia/Shanghai,不然时区问题会引发一系列时间写入异常。

另外,Linux 上的 MySQL 默认对库名和表名区分大小写,Activiti 的建表脚本默认使用小写表名,如果你在 Windows 本地建表时使用了大写、然后同步到 Linux 环境,可能在后续操作时报“表不存在”。建议数据库配置里设置lower_case_table_names=1,并规范表名都是小写。

5.6 流程启动时提示找不到流程定义

这是新手集成最容易困惑的地方。明明流程文件放在processes目录下,也确认了自动扫描配置,但调用startProcessInstanceByKey时报“no process definition found with key”。

排查重点是:自动部署到底有没有执行成功。可以先打印repositoryService.createProcessDefinitionQuery().list(),看返回结果里有没有对应 key。如果列表里有,很可能是调用时 key 写错了,注意大小写完全一致;如果列表为空,大概率是 BPMN 文件解析失败导致自动部署被跳过,此时要注意查看应用启动日志中是否有Deployment相关的异常信息,也要检查流程文件后缀名是否被识别。

有时候项目里多个模块分别放了自己的 BPMN 文件,资源路径不合适导致文件没被扫描到,也要确认process-definition-location-prefix的实际路径和文件位置匹配。

最后再分享一个实际项目中使用的小技巧

在我接手过一个老系统改造后,意识到一个特别重要的工作习惯:流程定义文件一定要纳入版本管理,并且每调整一次 BPMN,就要更新一次流程版本,生产环境不要轻易覆盖旧版本。Activiti 每次部署都会生成新的版本号,旧实例继续走旧逻辑,新单走新逻辑,这种并行兼容能力在业务迁移时极其好用。再加上我习惯用businessKey把业务主键贯穿始终,任何时刻想排查一张单走到哪个环节,只需要一个查询接口就能把运行实例、历史活动、当前待办串联起来。

指望一个下午就把工作流引擎带来的所有能力吸收掉不现实,但搭起来一个可用的 SpringBoot + Activiti 7 环境、跑通一个最简单的审批流程,完全是可以的。后面再慢慢啃 BPMN 的各种事件、子流程、会签和审批策略,遇到问题回来再翻这篇文章里的避坑点,应该能帮你少走一大段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询