warm-flow自研工作流引擎:6张表搞定轻量级审批流
2026/9/16 7:51:04 网站建设 项目流程

简介:一套面向中小型项目的国产自研 warm-flow 工作流设计源码,主打简洁易扩展,仅依赖6张基础表即可完成流程设计、任务调度、审批流转与状态管理等核心能力。压缩包共435个文件,大小约1.84MB,其中272个Java源文件负责业务逻辑,54个XML配置可灵活调整系统行为,33个JS与11个Vue组件组成前端交互界面,12个SQL脚本支撑数据持久化,另有warm-flow-ui、warm-flow-plugin等扩展件,整体结构清晰、组件独立,便于按需裁剪和二次开发。目前已有1002人学习下载,适合需要在中小型项目中快速引入或自研工作流模块的Java后端/全栈开发者。通过该源码可以深入学习轻量级工作流的分层设计、基于6张表的精简建模思路,以及插件机制与可视化界面的扩展方式;同时完善的目录划分和开源规范文件(readme、license等)能帮助开发者降低理解成本,快速将工作流能力集成到实际业务系统中。

1. 简洁到只用6张表,warm-flow 凭什么敢叫自研工作流

如果只是做个请假审批,引入 Flowable 就像给自行车装上波音747的引擎。warm-flow 这个国产自研工作流引擎,用 6 张基础表就把流程定义、任务调度、状态管理串了起来,全部源码只有 215 个文件,其中 Java 源码 158 个,没有复杂的模块矩阵,也没有动辄几十张表的元数据模型。第一次拆它的包结构时,最直观的感受是它把“简单够用”和“可扩展”两件事平衡得很好。中小项目做 OA、报销、合同审批时,往往只需要一套能被业务代码调用的流程引擎,而不是 BPMN 2.0 全家桶。下面就从源码文件布局、自动装配机制、表达式策略、API 用法和并发优化几个方向,把 warm-flow 的骨架拆开,看它是怎么做到既轻量又能接入真实业务的。

2. 从文件清单看架构:spring.factories 与 ExpressionStrategy 如何支撑扩展性

2.1 文件布局:215个文件里的分层线索

拿到源码包后,目录结构与常规 Spring Boot 工程很接近。.env.development是前端环境的变量样例,.editorconfig统一了多 IDE 的代码风格,对多人协作很重要。com.warm.flow.core.expression.ExpressionStrategy这个类路径直接暴露了核心包的命名空间。整体文件可以分成三块:核心模块负责流程引擎和持久化,UI 模块提供可视化流程设计器,插件模块用来挂接监听器和扩展策略。

158 个 Java 文件并没有堆在同一个包里,而是按coredaoserviceentitylistenerexpression等职责做了划分。这样当你只想要一个嵌入式流程引擎时,可以直接排除 UI 和插件依赖,核心模块依然能独立运行。对于中小项目,最后打出来的 jar 体积会比 Flowable 小一个数量级。

2.2 自动装配与组件隔离:spring.factories 的用法

warm-flow 能够做到“引入依赖即用”,核心是靠 Spring Boot 的自动装配。spring.factories中一般会写:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.warm.flow.config.WarmFlowAutoConfiguration

启动时,Spring Boot 会加载WarmFlowAutoConfiguration,由它负责注入数据源、初始化流程引擎、注册默认策略。这个设计让使用者不用手动创建任何引擎对象,对象生命周期完全交给容器管理。

组件独立还体现在排除机制上。如果暂时用不到插件,可以直接去掉warm-flow-pluginjar,引擎核心功能不受影响。新项目如果基于 Spring Boot 3.x,官方推荐使用AutoConfiguration.imports文件,spring.factories虽然仍被兼容,但建议按照新方式迁移,避免将来升级时出现警告。

2.3 表达式策略:ExpressionStrategy 接口的设计要点

工作流的“下一步去哪”通常由条件表达式决定。warm-flow 没有把表达式引擎写死,而是抽象了一个策略接口。从包名com.warm.flow.core.expression.ExpressionStrategy能看到它属于核心能力,接口大致是这样的:

public interface ExpressionStrategy { String getType(); boolean evaluate(String expression, Map<String, Object> context); }

getType()返回策略名,比如spelamountgroovyevaluate()根据表达式和上下文变量计算结果。流程定义里只需要保存策略名,运行时引擎会从 Spring 容器里收集所有ExpressionStrategy实现,按策略名派发。

内置的默认实现一般依赖 Spring 的SpelExpressionParser,适合大多数场景。如果项目里已经用了 Aviator 或者自研的轻量规则引擎,新增一个实现类并注册到 Spring 容器即可。这种方式规避了“表达式引擎升级时引发全量改动”的坑,也是 warm-flow 扩展性最直接的体现。

2.4 表结构与核心数据模型:六张基础表的职责划分

6 张表是 warm-flow 的核心卖点。下面是我在工程中比较认可的一套表模型,职责划分与 warm-flow 的设计思路基本一致:

表名职责关键字段
flow_definition流程定义id, flow_code, flow_name, version
flow_node流程节点id, definition_id, node_code, node_type, expression_strategy
flow_instance流程实例id, definition_id, business_key, status
flow_task待办任务id, instance_id, node_code, assignee, status, version
flow_history任务流转历史id, instance_id, task_id, operate_type, create_time
flow_variable流程变量id, instance_id, variable_key, variable_value

这 6 张表的链路关系是:flow_definition定义流程长什么样,flow_instance记录某次业务走到哪,flow_task表示当前谁在处理,flow_history记录每一步审批痕迹,flow_variable存放表达式中用到的金额、申请人等动态数据。节点间跳转关系和条件全部放在flow_node中,省掉了单独一张“连线表”,这是表数量能压到 6 张的直接原因。

这种建模的取舍是把权限直接简化为assignee字段,不设计角色、用户、组关系表。如果业务需要组织架构,可以在外部系统维护,进入引擎前把成员列表算好传给 assignee。在引擎内部做复杂权限,恰恰是 Activiti 显得臃肿的原因之一。

2.5 数据库方言与 SQL 文件适配

源码包中带了 7 个 SQL 文件,这一点很关键。工作流引擎最容易踩的坑就是数据库方言:MySQL 分页用 LIMIT,Oracle 用 OFFSET FETCH,SQL Server 的写法又不一样。把建表脚本和 Mapper XML 按数据库拆分,是让引擎同时跑在多种数据库上的常见做法。

业务项目里,我通常只保留自己环境的 SQL。试用时如果启动后没有自动建表,优先确认init-sql是否配到了正确的classpath路径,或者手动执行一次建表脚本。注意不要重复执行,否则会主键冲突。表前缀最好在一开始确定下来,中途修改会导致 Mapper XML 里的 SQL 全部需要跟着调整。

3. 落地实现:Spring Boot 集成 warm-flow 并跑通第一个审批流

3.1 环境准备与依赖引入

先编译源码包,把它安装到本地 Maven 仓库。进入解压后的目录执行:

cd warm-flow mvn install -DskipTests

执行成功后再在业务项目里引入核心依赖:

<dependency> <groupId>com.warm.flow</groupId> <artifactId>warm-flow-core</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>

需要可视化设计器和插件能力时,再分别引入warm-flow-uiwarm-flow-plugin。这里的版本号以你本地mvn install后输出的实际版本为准,不要照抄示例。

3.2 数据库初始化和配置参数

建好数据库后,执行对应数据库的 SQL 脚本。以 MySQL 为例,Spring Boot 配置如下:

spring: datasource: url: jdbc:mysql://localhost:3306/warm_flow username: root password: root warm-flow: table-prefix: flw_ init-sql: classpath:sql/mysql.sql

table-prefix用于多套系统共用一个数据库时区分表名,避免冲突。init-sql可以在启动时自动执行建表脚本,但有一个容易忽略的细节:脚本内部建表语句的表名不会自动加上前缀。如果你改了table-prefix,必须同步修改 SQL 文件里的表名,比如把flow_definition改成flw_flow_definition,否则启动后会报“表不存在”。

3.3 用代码定义一条“请假→审批”流程

warm-flow 的流程定义支持 XML 和流式 API 两种方式。我更推荐流式 API,因为编译期就能发现节点命名错误。一条最简单的请假审批流如下:

FlowDefinition definition = FlowBuilder.build("leave") .flowName("请假审批") .node("start").name("开始").type("start") .node("apply").name("员工申请").type("userTask") .assignee("${applyUser}") .node("manager").name("经理审批").type("userTask") .assignee("${managerUser}") .node("end").name("结束").type("end") .condition("amount > 1000", "apply", "manager") .condition("amount <= 1000", "apply", "end") .save();

assignee("${applyUser}")使用的是流程变量占位符,发起时会从变量集合中把applyUser替换成实际工号。condition的语义是:当金额大于 1000 时,从apply节点跳转到manager节点,否则直接到end。条件表达式默认走 SpEL 策略,如果字符串里混入类型不匹配的数据,运行期会抛出转换异常。

3.4 发起流程与完成任务的核心 API

流程定义保存好后,业务侧发起一次申请:

FlowInstance instance = flowRuntimeService.startProcessInstance( "leave", "BIZ20250314001", Map.of("applyUser", "U1001", "managerUser", "M2001", "amount", 800));

参数从左到右分别是流程编码、业务主键、流程变量。流程编码对应flow_definition.flow_code,业务主键用于后续回查业务表,流程变量参与节点条件判断和任务负责人分配。

完成任务时,需要一个任务 ID 和当前操作人:

flowTaskService.complete("T10001", "U1001", Map.of("amount", 800));

complete执行前,引擎会校验任务是否待办、操作人是否与assignee匹配。实际业务中要避免硬编码任务 ID,正确做法是先查待办列表拿到任务 ID,再执行完成操作。

3.5 查询待办与历史记录

待办列表是工作流最高频的查询。接口风格类似 MyBatis-Plus:

FlowPage<Task> todoPage = flowTaskService.todoList( FlowPage.page(1, 10), Wrappers.lambdaQuery(Task.class) .eq(Task::getAssignee, "U1001") .ne(Task::getStatus, "done"));

todoList会过滤掉已完成和已挂起的任务,直接贴合“待办”语义。第一个参数是分页对象,第二个参数是条件构造器,可继续拼接时间范围、节点编码等条件。

历史记录查询推荐直接读取flow_history

List<History> historyList = flowHistoryService.list( Wrappers.lambdaQuery(History.class) .eq(History::getInstanceId, instance.getId()) .orderByAsc(History::getCreateTime));

flow_history是只增不改的,每次任务创建、完成、驳回都会写入一条记录,不要对它做更新操作。前端的时间线可以直接用这个结果渲染。

下面是常用 API 的速查表:

方法作用关键参数
startProcessInstance发起流程flowCode, businessKey, variables
complete完成任务taskId, operator, variables
todoList查询待办page, queryWrapper
historyList查询历史instanceId, order

3.6 驳回与任意跳转的处理思路

只走直线流程的项目很少,驳回是刚需。warm-flow 的节点类型允许定义驳回目标,常见做法是在任务对象上加一个permittedRejectTargets字段,保存当前节点允许驳回的节点集合,接口调用时做合法性校验,避免用户乱跳。

如果需要在运行期强制改变实例方向,可以调用实例服务把当前节点切到目标节点。但这种操作在生产环境要非常谨慎,它会跳过原路径上的其他审批节点。建议在业务层封装一层操作日志,把每次强制跳转的操作者、原因、目标节点都记录到自有表里,故障时才能回溯。

4. 扩展点实战:从流程适配器到自定义条件策略

4.1 为什么说组件独立:模块依赖关系

“组件独立”意味着核心模块没有和 Spring Web 层、持久层实现强绑定。阅读源码时注意com.warm.flow.core下主要是接口和抽象类,MyBatis 的具体操作被拆到dao层,并注入SqlSessionTemplate。如果项目用 JPA 而不是 MyBatis,可以仿照 Mapper 接口重新实现数据操作,不需要改动引擎的调度逻辑。

这种依赖倒置关系是扩展性的基础。流程引擎只关心FlowTaskMapper接口,不关心背后是 MyBatis 还是 JdbcTemplate。很多自研工作流之所以难扩展,就是因为 service 层的大类里堆满了持久化细节。warm-flow 把接口与实现分离,替换存储层时只需要新写一套 mapper 实现。

4.2 实现一个自有表达式策略

假设你不喜欢 SpEL 的语法,希望直接支持amount>1000这种紧凑写法。定义一个策略实现ExpressionStrategy

@Component public class AmountExpressionStrategy implements ExpressionStrategy { @Override public String getType() { return "amount"; } @Override public boolean evaluate(String expression, Map<String, Object> context) { String[] parts = expression.split(">"); BigDecimal actual = new BigDecimal(context.get(parts[0].trim()).toString()); return actual.compareTo(new BigDecimal(parts[1].trim())) > 0; } }

然后在流程定义里通过重载指定策略名:

.condition("amount>1000", "apply", "manager", "amount")

如果不传策略名,默认会走spel。这里特意用BigDecimal而不是Double,因为金额比较用浮点数会出精度问题。表达式解析失败时,优先检查流程变量是否真的传了对应 key,以及变量值是否能转换成预期类型。

4.3 通过插件机制接入业务逻辑

插件模块是 warm-flow 里常被忽略的亮点。它允许在任务创建、完成、驳回等生命周期节点插入业务行为。插件接口大致如下:

public interface FlowTaskListener { default void onTaskCreated(Task task) {} default void onTaskCompleted(Task task) {} default void onTaskRejected(Task task) {} }

在 Spring Boot 工程中,实现类注册成 Bean,并通过spring.factories声明:

com.warm.flow.plugin.FlowTaskListener=\ com.example.listener.WeComNotificationListener

WeComNotificationListener里可以根据节点编码判断是否发送企微通知。业务通知逻辑和引擎完全隔离,后续去掉通知功能时,只需要移除插件依赖,引擎代码不受影响。这种方式比在 service 里写if (type.equals("notify"))干净得多。

4.4 多人会签的轻量实现

中小项目中常见的会签需求,不需要引入额外的流程引擎能力。我一般用flow_variable表存计数器,进入会签节点时初始化countpassed,每名审批人完成任务时更新两个变量,当passed达到阈值后自动跳转。监听器里可以实现这段逻辑:

public void onTaskCompleted(Task task) { Map<String, Object> variables = flowRuntimeService.getVariables(task.getInstanceId()); int passed = (int) variables.getOrDefault("passed", 0); variables.put("passed", ++passed); int count = (int) variables.getOrDefault("count", 1); if (passed >= count) { flowTaskService.complete(task.getId(), task.getAssignee(), variables); } }

这个示例展示的是串行会签写法。如果要求并行会签,warm-flow 原生不支持并行网关时,需要拆成多个审批节点,并在最后一个节点完成时统一加签。这也是选型时要考虑清楚的边界。

4.5 与 Flowable/Activiti 怎么选

维度warm-flowFlowable / Activiti
表数量6 张70+ 张
BPMN 2.0部分或非严格完整支持
学习曲线
扩展策略接口式配置驱动 + 事件
适合场景中小项目、嵌入式复杂流程平台
运行体积

如果团队里都是熟悉 Spring Boot 的工程师,但没有专门研究过工作流规范,用 warm-flow 可以在一周内把审批流跑起来。如果流程引擎未来要作为低代码平台底座,需要支持复杂的定时器、子流程、多实例会签,那 Flowable 仍然是更稳妥的选择。warm-flow 的价值不在于替代 Flowable,而是在它够用的场景里把复杂度降到最低。

5. 性能优化与排错技巧:从日志到并发控制

5.1 控制台日志与常见异常定位

使用 warm-flow 时,把包级别日志调到 DEBUG 能快速定位问题:

logging: level: com.warm.flow: debug

最常见的异常有两种。第一种是找不到表,通常是init-sql没生效或者表前缀配置不一致,启动日志里会打印实际的 SQL 执行记录,对照检查即可。第二种是表达式解析异常,比如流程变量里传了字符串"abc",表达式却执行了数值比较,此时要回到流程定义处检查condition中的字段名和变量 key 是否完全一致。

5.2 六张表的索引设计建议

表数量少不等于不需要索引。下面三组索引是必须加的:

ALTER TABLE flow_instance ADD INDEX idx_instance_biz (business_key); ALTER TABLE flow_task ADD INDEX idx_task_assignee_status (assignee, status); ALTER TABLE flow_history ADD INDEX idx_history_instance_id (instance_id);

理由很直接:流程实例经常用business_key反查,待办列表的查询条件基本是assignee + status,历史记录总是按instance_id聚合。不加索引时,流程量过万就会出现慢查询,尤其在任务表上。

5.3 任务重复提交与乐观锁处理

最后一个高频坑是重复提交。前端连点两次、MQ 重试都可能让同一个任务被complete两次。warm-flow 的flow_task表通常带有version字段,更新时把版本号作为条件:

int rows = flowTaskService.update() .eq(Task::getId, taskId) .eq(Task::getVersion, version) .set(Task::getStatus, "done") .set(Task::getVersion, version + 1) .update(); if (rows == 0) { throw new BusinessException("任务已被处理,请勿重复提交"); }

影响行数为 0 说明版本号已经被其他请求修改,直接拒绝。这种乐观锁方案在任务更新场景下足够,不需要引入 select for update。如果并发量极高,再配合 Redis 分布式锁做一层请求级别的兜底,双保险后基本不会再出现重复审批。

本文还有配套的精品资源,点击获取

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

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

立即咨询