上周帮一个团队把审批中心从 if-else 硬编码改造成工作流引擎驱动,我选了 Activiti 7。这里说句实话,如果不是已经在一个 SpringBoot 项目里做了两年多流程相关功能,我可能也会被网上那些过时教程带偏——Activiti 7 和你在老博客里刷到的 Activiti 5/6 集成方式,已经是两种不同的东西。这篇文章把我从项目搭建、流程部署、任务流转到生产排障的完整过程整理出来,给准备在 SpringBoot 项目中接入 Activiti 7 工作流引擎的同学做个参考。文章不写废话,每个配置、每段代码都是跑过验证的。
1. 集成之前必须先认清Activiti 7的架构变化
1.1 与Activiti 5/6最大的不同:集成方式与安全体系
老版本的 Activiti(5.x、6.x)集成 Spring Boot,核心思路是引入一个 starter,然后自己配 ProcessEngine、包扫描、服务注册,社区里大量教程都停在这个年代。到了 Activiti 7,架构做了大调整:引擎本身被拆成了多个模块,Spring Boot 集成变成了官方主推的一等公民,starter 里默认带上 Spring Security 依赖,认证方式也从以前自己写拦截器变成了直接吃 SecurityContext 里的用户信息。
这个变化直接影响你写代码的方式。比如,Activiti 7 中任务办理接口拿到当前操作人,走的是org.activiti.api.runtime.shared.security.SecurityManager,它内部从 Spring Security 的 Authentication 里取用户名。如果项目里没有引入 Spring Security,或者引入了但没做好认证上下文,那你调用 TaskRuntime、ProcessRuntime 这些新 API 时,会拿到一个空用户或者直接报 401。这是很多第一次用 Activiti 7 的人最容易栽跟头的地方。
另一个变化是版本号。Activiti 7 的正式版本长期停留在里程碑版本,比如7.1.0.M6。网上有人看到 M 后缀就不敢用,其实实际项目里跑一两年没问题的案例不少,关键是依赖锁定好,别混合版本。
1.2 为什么不直接选Flowable:同源分流的现实取舍
聊 Activiti 7 的时候绕不开 Flowable,因为两者同源。Activiti 5/6 时代一部分核心代码被 fork 出去,成了后来的 Flowable。现在去搜流程引擎方案,Flowable 因为迭代节奏更快,在社区里声音也更大。
但我的选择逻辑很直接:团队里没人深入研究过 Flowable,而 Activiti 的资料、示例、书籍更多,招人也好招。Flowable 和 Activiti 的表结构确实相似,BPMN 2.0 规范也都是同一套,关键能力比如会签、驳回、多实例、定时器都是齐的。如果你不是遇到 Activiti 确实搞不定的需求,不建议为了“更新”而换 Flowable,换来换去反而多踩一遍坑。把 Activiti 7 在 SpringBoot 里吃透,换成 Flowable 的迁移成本也不会太高,因为 API 结构非常接近。
2. 依赖引入与配置文件:版本匹配是第一道坎
2.1 依赖清单:官方starter还不够,必须引入BOM
新建一个 SpringBoot 项目后,第一步不是直接抄一个 starter 进 pom.xml,而是先把 Activiti 的 BOM 引入依赖管理。不然你后面会遇到各种传递依赖版本冲突,尤其是 Spring Security 相关 jar。
我实际项目里的 pom 关键部分是这样配的:
<dependencyManagement> <dependencies> <dependency> <groupId>org.activiti.dependencies</groupId> <artifactId>activiti-dependencies</artifactId> <version>7.1.0.M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>注意这里没有手动写 Activiti 的 version,交给 BOM 管理。否则你要是单独写个7.1.0.M6,很容易和 Spring Boot 的 spring-boot-starter-parent 里的依赖版本打架,尤其是在 spring-security 上。
Spring Boot 版本这边,我的实测结论是:Spring Boot 2.5.x 和 Activiti 7.1.0.M6 最稳。用 Spring Boot 2.7.x 也能跑,但一旦你的项目里还有其它和 Spring Security 强相关的组件,比如 springdoc、oauth2 资源服务器,启动时经常出现NoSuchMethodError。这不是你代码写错了,纯粹是 jar 包版本不对齐,下面第 5 章我会讲一个具体的排查案例。
2.2 application.yml里的那些关键开关
配置文件里,很多人只抄了spring.datasource和spring.activiti.database-schema-update,结果启动后要么自动部署不生效,要么历史数据查不到。我把自己验证过的一套配置贴出来:
spring: datasource: url: jdbc:mysql://localhost:3306/workflow?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver activiti: database-schema-update: true db-history-used: true history-level: full process-definition-location-prefix: classpath:/processes/ check-process-definitions: true async-executor-activate: false逐项解释一下为什么这么配:
database-schema-update: true表示启动时自动建表或升级表结构,开发环境非常方便。但生产环境必须改成false,或者用true配合受控发布窗口,否则引擎会在启动时偷偷改表结构,DBA 根本来不及 review。
db-history-used: true配合history-level: full,保证流程实例、任务、活动、变量的历史全部落库。你后面做流程轨迹回放、统计报表全靠这两项。history-level如果写audit,部分变量变化记录会缺失,排查问题时会很痛苦。
async-executor-activate: false是我的习惯。Activiti 有独立的定时任务线程池,处理定时器、异步延续等。如果你的流程定义里没有定时器场景,没必要启动它,省一点无用线程开销。等真的需要时再改成 true。
process-definition-location-prefix: classpath:/processes/是固定约定,Activiti 启动时会扫描这个目录下的.bpmn20.xml或者.bpmn文件,做自动部署。
2.3 数据源与数据库准备:自动建表不是万能的
Activiti 7 需要至少 5 张核心表,实际会生成差不多 40 多张,表名前缀是ACT_开头,分成ACT_RE_(流程定义、模型、部署)、ACT_RU_(运行时数据)、ACT_HI_(历史数据)、ACT_GE_(通用数据)。数据库连接串里我加了nullCatalogMeansCurrent=true,这个参数主要解决 MySQL 8 下的一些 catalog 兼容问题。
如果你用的是 MySQL 8.0,驱动必须是com.mysql.cj.jdbc.Driver,老驱动com.mysql.jdbc.Driver会直接报连接异常。还有字符集的问题,建库语句最好显式指定:
CREATE DATABASE workflow DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4 比 utf8 更稳妥,因为流程变量里可能存表情字符,比如审批备注里带个 emoji,普通 utf8 在某些字符集排序规则下会报 “Incorrect string value”。
3. 从BPMN文件到流程实例:核心API这样用
3.1 一个可复用的请假审批流程BPMN模板
Activiti 执行的最小单位是 BPMN 2.0 文件。我先给一个可以直接部署的请假流程,它麻雀虽小五脏俱全:开始事件、用户任务、排他网关、结束事件,还带条件表达式。
<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" targetNamespace="http://www.example.com/process"> <process id="leaveProcess" name="请假审批流程" isExecutable="true"> <startEvent id="startEvent" name="开始"/> <userTask id="applyTask" name="填写请假单" activiti:assignee="${applyUser}"/> <userTask id="managerTask" name="部门经理审批" activiti:assignee="${managerUser}"/> <exclusiveGateway id="daysGateway" name="天数判断"/> <userTask id="hrTask" name="人事备案" activiti:assignee="${hrUser}"/> <endEvent id="endEvent" name="结束"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="applyTask"/> <sequenceFlow id="flow2" sourceRef="applyTask" targetRef="managerTask"/> <sequenceFlow id="flow3" sourceRef="managerTask" targetRef="daysGateway"/> <sequenceFlow id="flow4" sourceRef="daysGateway" targetRef="hrTask"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days >= 3}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="flow5" sourceRef="daysGateway" targetRef="endEvent"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${days < 3}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="flow6" sourceRef="hrTask" targetRef="endEvent"/> </process> </definitions>这个文件放在src/main/resources/processes/leave-process.bpmn20.xml。注意文件名后缀必须是.bpmn20.xml或者.bpmn,Activiti 自动部署时靠后缀识别。流程 ID 是leaveProcess,后面启动流程、查询流程定义都用这个 key,不能乱改。
3.2 RepositoryService:部署与版本管理
流程文件放好后,一般不需要手动部署,因为配置了check-process-definitions,启动时引擎会自动扫目录。但如果你们流程文件是从数据库读取的,或者需要做灰度发布,那必须手动用RepositoryService部署:
@Autowired private RepositoryService repositoryService; public String deployProcess(InputStream inputStream, String name) { Deployment deployment = repositoryService.createDeployment() .name(name) .addInputStream("leave-process.bpmn20.xml", inputStream) .category("oa") .deploy(); return deployment.getId(); }部署之后,同一个流程 ID 的新版本会生成新的ACT_RE_PROCDEF记录,版本号递增。每次启动新流程实例时,默认使用最新版本。查询所有版本可以这样:
List<ProcessDefinition> definitions = repositoryService.createProcessDefinitionQuery() .processDefinitionKey("leaveProcess") .orderByProcessDefinitionVersion() .desc() .list();这里有一个容易被忽略的点:旧版本流程已经启动的实例,在流转过程中默认还是按旧版本的 BPMN 走。也就是说,你改了流程定义,不影响已经跑了一半的流程,只是新起流程时用新版本。这是引擎的标准行为,不用慌。
3.3 RuntimeService:启动实例并向流程传参
启动一个流程实例,对应业务上就是“张三发起一个请假申请”。需要把流程变量一并传进去,尤其注意 BPMN 里用到的${applyUser}、${managerUser}、${days}这些变量,引擎在走到对应节点时会实时读取。
@Autowired private RuntimeService runtimeService; public ProcessInstance startLeaveProcess(String applicant, String manager, int days) { Map<String, Object> variables = new HashMap<>(); variables.put("applyUser", applicant); variables.put("managerUser", manager); variables.put("days", days); // 这里第二个参数是业务key,方便业务系统关联 return runtimeService.startProcessInstanceByKey("leaveProcess", variables); }有一个高频翻车点:${applyUser}如果没传,流程不会在启动时立刻报错,而是当执行到“填写请假单”这个任务时,引擎尝试解析表达式applyUser,发现没有这个变量,直接抛ActivitiException或者流程卡住。所以,流程变量的完整性要在启动前用代码做校验,不要依赖引擎自动兜底。
启动成功后,返回的ProcessInstance对象里有processInstanceId,这个 ID 是后面查询任务、回滚、查看历史的通行证,记得存到业务表。
3.4 TaskService:查询待办与完成任务
TaskService 是日常代码里调用最频繁的接口。查询张三的待办任务:
@Autowired private TaskService taskService; public List<Task> getTodoTasks(String assignee) { return taskService.createTaskQuery() .taskAssignee(assignee) .processDefinitionKey("leaveProcess") .active() .orderByTaskCreateTime() .desc() .list(); }需要注意:这里.active()过滤掉挂起任务,.taskAssignee()是精确匹配“指定人”。如果你用候选组或者候选人模式,查询方式不一样。最简单粗暴的做法就是给每个任务节点指定具体审批人,适合中小团队;如果要做角色组审批,就用taskCandidateGroup("manager"),后面再说。
完成任务时,可以同时传流程变量或者局部变量:
public void completeTask(String taskId, boolean approved, String comment) { Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); if (task == null) { throw new RuntimeException("任务不存在或已被办理"); } Map<String, Object> vars = new HashMap<>(); vars.put("approve", approved); vars.put("comment", comment); taskService.addComment(taskId, task.getProcessInstanceId(), comment); taskService.complete(taskId, vars); }这里我习惯在 complete 前先 addComment,把审批意见落到引擎的ACT_HI_COMMENT表,后面追溯时能直接查。complete传入的vars默认是流程实例级变量,会直接影响后面网关条件的判断,所以要看清你传的变量名和 BPMN 里的条件表达式是否一致。
4. 审批场景里的“驳回”和“会签”到底怎么实现
4.1 驳回:没有现成API,但有三条常见路子
很多第一次用 Activiti 的人会问“引擎有没有内置驳回接口”。答案是:没有“一键驳回”这种 API。Activiti 只提供流程实例推进的标准动作,驳回本质上是“流程跳转”,要靠流程模型设计或底层 API 实现。
我在实际项目里用过三种方案,按推荐程度排序:
第一种,最推荐,在 BPMN 里设计回退网关。在经理审批节点后面加一个排他网关,根据审批结果决定是回退到申请节点还是继续往下走。流程定义大概长这样:
<exclusiveGateway id="resultGateway" name="审批结果判断"/> <sequenceFlow id="rejectFlow" sourceRef="resultGateway" targetRef="applyTask"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == false}]]></conditionExpression> </sequenceFlow> <sequenceFlow id="approveFlow" sourceRef="resultGateway" targetRef="daysGateway"> <conditionExpression xsi:type="tFormalExpression"><![CDATA[${approved == true}]]></conditionExpression> </sequenceFlow>经理审批时 complete 传入approved=false,流程会自动回到 “填写请假单” 节点,申请节点重新生成一个新任务。这个方案的可控性最强,业务规则一眼可见。
第二种,使用RuntimeService的ChangeActivityStateBuilder,适合动态跳转场景:
runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo("managerTask", "applyTask") .changeState();这个 API 比较底层,它会把当前活动节点 managerTask 迁移到 applyTask,删掉当前活动,重新生成目标活动。复杂流程里如果多个分支并行,很容易迁错,我只在极少数动态会签场景里用,日常建议走第一种。
第三种,是打补丁式:把流程实例挂起,手动改ACT_RU_TASK表任务节点,再激活。这种我强烈不建议碰,数据库里改完经常出现执行实例和任务状态对不上,排查成本极高。
4.2 会签/或签:多实例节点的XML配置
会签是审批流程里的高频需求。比如采购金额大于一定阈值,需要三名部门经理全部同意才通过;或者是签,只要一个人同意就通过。Activiti 用multiInstanceLoopCharacteristics实现。
<userTask id="countersignTask" name="会签审批" activiti:assignee="${assigneeUser}"> <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${countersignUsers}" activiti:elementVariable="assigneeUser"> <completionCondition>${nrOfCompletedInstances >= 2}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>这段配置的含义是:集合变量countersignUsers里有多少人,就并行生成多少个待办任务,每个任务指派给assigneeUser(即集合里的每一个人)。等完成数量达到 2 个,整个会签节点就提前流转。如果去掉completionCondition,默认是集合里所有人都完成才流转。
启动流程时,需要传集合变量:
Map<String, Object> vars = new HashMap<>(); vars.put("countersignUsers", Arrays.asList("userA", "userB", "userC")); vars.put("applyUser", "zhangsan"); runtimeService.startProcessInstanceByKey("leaveProcess", vars);办理每一个人任务时,还是走taskService.complete(taskId, variables)。这里有个小技巧:如果想记录每个人在会签中的独立意见,用局部变量,不要用流程级变量。
taskService.setVariableLocal(taskId, "opinion", "同意"); taskService.complete(taskId);这样每个子任务的局部变量互不影响,历史表里能清楚看到谁签了什么意见。等completionCondition触发后,nrOfCompletedInstances会带着所有子任务的结果一起汇总。
4.3 流程变量作用域:很多人在这里翻车
流程变量有三种常见作用域:流程实例级、执行实例级、任务级。简单记:setVariable是流程实例级,全局可见;setVariableLocal是当前执行实例或任务级,局部可见。
最开始带我的老师傅说过一句让我印象很深的话:“流程变量不是你放在 HashMap 里就能随便读的,变量作用域理解错了,等于把钥匙藏在别人兜里。” 实操中,我在会签节点踩过一个坑:用setVariableLocal存了审批意见,结果在会签完成后的排他网关里读不到,因为网关是在父执行实例上求值,读的是流程实例级变量。正确做法是,影响流程走向的数据统一用setVariable存流程实例级,只有各节点内部处理用的临时数据才用局部变量。
5. 我在实际项目中踩过的几个坑
5.1 启动报NoSuchMethodError:jar包冲突排查链路
我接手的那个项目,Spring Boot 版本是 2.7.18,引入 Activiti 7.1.0.M6 后,应用启动直接报:
java.lang.NoSuchMethodError: org.springframework.security.web.SecurityFilterChain这个问题最可恨的点在于,报错信息和你的业务代码毫无关系,完全看不出来是谁触发的。我当时的排查链路分享给你:
第一步,看完整堆栈,找到第一个出现Caused by的位置,确认是 Spring Security 相关类加载冲突。第二步,跑mvn dependency:tree -Dincludes=org.springframework.security,看到 activiti-spring-boot-starter 传递进来的 spring-security-web 版本是 5.3.x,而 Spring Boot 2.7.18 期望的 SecurityFilterChain 是 5.7.x 的 API,两个版本在SecurityFilterChain的接口定义上发生了二进制不兼容。第三步,解决方案有两条路:把 Spring Boot 降到 2.5.5 左右,或者用 dependencyManagement 手动锁定 spring-security 版本。我当时选择了把 Spring Boot 从 2.7.18 降到 2.5.5,问题消失,整个项目不再出现类似冲突。
这里我要强调一个观念:SpringBoot 版本不是越新越好,要看你集成的中间件支持到什么版本。Activiti 7 官方支持矩阵当时主要对齐 Boot 2.x 的中期版本,你硬上 Boot 2.7 就是给自己找事。
5.2 流程图中文乱码:字体文件缺失
流程部署后,在流程跟踪页面看流程图,节点名称的中文全部变成方框。一开始我怀疑是数据库字符集问题,检查了一圈发现 BPMN 文件是 UTF-8,表也是 utf8mb4,数据库没问题。
后来才定位到,Activiti 7 生成流程图时用的是 Java 的图形库,默认字体是 Dialog,这种字体在 Linux 服务器上不包含中文字形,画图时中文就变成方块。解决办法是给 ProcessEngineConfiguration 指定中文字体:
@Bean public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer() { return config -> { config.setActivityFontName("宋体"); config.setLabelFontName("宋体"); config.setAnnotationFontName("宋体"); }; }如果你用的 Docker 镜像比较精简,可能连宋体都没装,那还要在镜像里装fonts-wqy-microhei之类的中文字体包。这个问题在本地 Windows 开发环境不一定能复现,到 Linux 环境才爆,所以别用“我本地正常”来推断服务器行为。
5.3 Spring Security拦截导致流程接口401
Activiti 7 的 starter 自带 spring-boot-starter-security 依赖,项目启动后控制台会打印 “Using generated security password: xxxx”,然后你所有工作流接口都是 401。
这个问题本质上是 Activiti 7 希望你提供认证用户。如果你项目里本身有登录系统,就把你已有的登录逻辑接入 Spring Security,让请求带着认证信息进来。如果只是内部管理系统演示,可以直接配置放行所有请求:
@Configuration public class ActivitiSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .anyRequest().permitAll() .and() .httpBasic(); return http.build(); } }但注意,这里只是解决了接口访问权限问题。如果你用了 ProcessRuntime 这类新 API,它内部还是要从 SecurityContextHolder 拿用户名,拿不到会报InsufficientAuthenticationException。所以从业务角度看,还是应该把当前登录用户塞进 SecurityContext,或者尽量用老牌的 RuntimeService、TaskService,它们不受 SecurityContext 强制约束。
5.4 查不到历史数据:history相关配置被漏掉
项目上线两周后,业务方要拉一个“所有已完成请假流程”的报表,结果HistoryService查出来是空的,但 ACT_RU_* 表有数据。
我们查了半天,最后发现application.yml里只配了spring.activiti.database-schema-update: true,没配db-history-used和history-level,默认值没有把完整历史记录下来。把配置补齐后重启,新产生的流程实例历史数据就正常了。注意,已经产生但没记录完整历史的流程实例不会自动补数据,只能存量数据做清洗,或者按业务表信息手工补录。所以历史配置一定要在项目早期就定好。
6. 生产级经验:监控、清理与性能优化
6.1 核心表结构速记:排查问题先看哪张表
工作流引擎出问题时,不要一根筋查代码,先看表。我把常用表整理成一张表,排查时按图索骥:
| 表名 | 归属 | 作用 |
|---|---|---|
| ACT_RE_DEPLOYMENT | 仓库 | 部署记录,一个部署对应一个文件包 |
| ACT_RE_PROCDEF | 仓库 | 流程定义,一个流程ID会有多个版本 |
| ACT_RU_EXECUTION | 运行时 | 执行实例,核心流程跳转状态 |
| ACT_RU_TASK | 运行时 | 待办任务,当前谁在处理 |
| ACT_RU_VARIABLE | 运行时 | 流程变量,当前运行中的变量值 |
| ACT_HI_PROCINST | 历史 | 历史流程实例,流程生命周期 |
| ACT_HI_TASKINST | 历史 | 历史任务实例,所有任务创建和完成时间 |
| ACT_HI_ACTINST | 历史 | 历史活动实例,每个节点的执行记录 |
| ACT_HI_VARINST | 历史 | 历史变量,流程变量变化后的最终值 |
| ACT_HI_COMMENT | 历史 | 审批意见和备注 |
举个例子:一个流程实例卡在“部门经理审批”没有流转,你可以先去 ACT_RU_TASK 看 assignee、create_time,再去 ACT_RU_EXECUTION 看当前活动节点,如果 ACT_RU_TASK 为空而 ACT_RU_EXECUTION 还有记录,说明执行实例卡在某个非用户节点上,典型的场景是排他网关没有匹配条件,流程没有可走的出口。这种问题在 BPMN 设计器里看不出来,跑起来才发现。
6.2 数据膨胀:历史表清理与归档策略
Activiti 的 ACT_HI_ACTINST 表增长速度吓人,一次流程流转经过五六个节点,就会产生五六个活动实例记录。半年下来,表轻松百万级数据,查询历史开始变慢。
不要等到卡了再后悔。我在项目里留了一个定时任务,按季度把三个月前的历史数据搬到历史库:
@Scheduled(cron = "0 0 2 1 * ?") public void archiveHistory() { Date threshold = DateUtils.addMonths(new Date(), -3); List<HistoricProcessInstance> oldInstances = historyService.createHistoricProcessInstanceQuery() .finishedBefore(threshold) .list(); for (HistoricProcessInstance instance : oldInstances) { historyService.deleteHistoricProcessInstance(instance.getId()); } }这里用deleteHistoricProcessInstance会级联删除该实例的历史活动、历史任务、历史变量,比较彻底。但如果业务需要保留审计轨迹,就不能这样删,应该把数据同步到归档表或者冷存储,再从引擎表物理删除。大批量删除时,ACT_HI_ACTINST 表还会涉及关联外键,建议一次删 5000 条左右循环处理,不要一次性删几十万行,会把 InnoDB 锁持有时间拉长,影响在线业务。
6.3 异步执行器与缓存参数调整
Activiti 7 引擎在启动时会初始化一个异步定时执行器,如果你流程里没有定时器、异步延续之类的需求,把它关掉最简单:spring.activiti.async-executor-activate: false。
流程定义缓存方面,默认情况下引擎会把最近用到的流程定义缓存到内存,避免每次任务都重新解析 BPMN。如果你的系统中有大量不同的流程定义,可以配置缓存上限,防止内存被顶爆:
spring: activiti: process-definition-cache-limit: 128另外,数据库层面,ACT_HI_ACTINST、ACT_HI_TASKINST 这种大表的查询很依赖索引。Activiti 建表脚本自带了一部分索引,但实际查询条件可能和默认索引对不上,比如你要按PROC_INST_ID_ + START_TIME_查历史活动,建议自己补一个组合索引。
ALTER TABLE ACT_HI_ACTINST ADD INDEX idx_proc_start_time (PROC_INST_ID_, START_TIME_);建完后用EXPLAIN验证一下执行计划,别盲目建一堆冗余索引,写入性能也会受影响。
最后再分享一个小技巧:如果你们的流程定义文件频繁改动,调试时想让探索环境自动重部署,可以写一个监听器,监听本地文件变更后调用repositoryService.createDeployment()重新部署,并且在 BPMN 里给流程定义加一个版本后缀,比如leaveProcess_v2,这样新老流程都能在引擎上共存,不会影响正在跑的旧流程实例。这个习惯我一直沿用到生产,大大降低了流程变更的上线风险。
我个人的体会是:Activiti 7 本身不复杂,复杂的是你所在业务系统里那些边界条件——审批人怎么定、超时怎么提醒、驳回能不能限制次数、会签通过率怎么算。先用透这篇文章里的基础链路,再把边界一个一个补上,流程引擎才能真正变成你业务的中枢,而不是另一个需要人维护的“复杂系统”。