
简介一套面向中小型项目的国产自研 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 文件并没有堆在同一个包里而是按core、dao、service、entity、listener、expression等职责做了划分。这样当你只想要一个嵌入式流程引擎时可以直接排除 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, MapString, Object context); }getType()返回策略名比如spel、amount、groovyevaluate()根据表达式和上下文变量计算结果。流程定义里只需要保存策略名运行时引擎会从 Spring 容器里收集所有ExpressionStrategy实现按策略名派发。内置的默认实现一般依赖 Spring 的SpelExpressionParser适合大多数场景。如果项目里已经用了 Aviator 或者自研的轻量规则引擎新增一个实现类并注册到 Spring 容器即可。这种方式规避了“表达式引擎升级时引发全量改动”的坑也是 warm-flow 扩展性最直接的体现。2.4 表结构与核心数据模型六张基础表的职责划分6 张表是 warm-flow 的核心卖点。下面是我在工程中比较认可的一套表模型职责划分与 warm-flow 的设计思路基本一致表名职责关键字段flow_definition流程定义id, flow_code, flow_name, versionflow_node流程节点id, definition_id, node_code, node_type, expression_strategyflow_instance流程实例id, definition_id, business_key, statusflow_task待办任务id, instance_id, node_code, assignee, status, versionflow_history任务流转历史id, instance_id, task_id, operate_type, create_timeflow_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 分页用 LIMITOracle 用 OFFSET FETCHSQL 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 groupIdcom.warm.flow/groupId artifactIdwarm-flow-core/artifactId version1.0.0-SNAPSHOT/version /dependency需要可视化设计器和插件能力时再分别引入warm-flow-ui和warm-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.sqltable-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-PlusFlowPageTask todoPage flowTaskService.todoList( FlowPage.page(1, 10), Wrappers.lambdaQuery(Task.class) .eq(Task::getAssignee, U1001) .ne(Task::getStatus, done));todoList会过滤掉已完成和已挂起的任务直接贴合“待办”语义。第一个参数是分页对象第二个参数是条件构造器可继续拼接时间范围、节点编码等条件。历史记录查询推荐直接读取flow_historyListHistory historyList flowHistoryService.list( Wrappers.lambdaQuery(History.class) .eq(History::getInstanceId, instance.getId()) .orderByAsc(History::getCreateTime));flow_history是只增不改的每次任务创建、完成、驳回都会写入一条记录不要对它做更新操作。前端的时间线可以直接用这个结果渲染。下面是常用 API 的速查表方法作用关键参数startProcessInstance发起流程flowCode, businessKey, variablescomplete完成任务taskId, operator, variablestodoList查询待办page, queryWrapperhistoryList查询历史instanceId, order3.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 的语法希望直接支持amount1000这种紧凑写法。定义一个策略实现ExpressionStrategyComponent public class AmountExpressionStrategy implements ExpressionStrategy { Override public String getType() { return amount; } Override public boolean evaluate(String expression, MapString, 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(amount1000, 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.WeComNotificationListenerWeComNotificationListener里可以根据节点编码判断是否发送企微通知。业务通知逻辑和引擎完全隔离后续去掉通知功能时只需要移除插件依赖引擎代码不受影响。这种方式比在 service 里写if (type.equals(notify))干净得多。4.4 多人会签的轻量实现中小项目中常见的会签需求不需要引入额外的流程引擎能力。我一般用flow_variable表存计数器进入会签节点时初始化count和passed每名审批人完成任务时更新两个变量当passed达到阈值后自动跳转。监听器里可以实现这段逻辑public void onTaskCompleted(Task task) { MapString, 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 分布式锁做一层请求级别的兜底双保险后基本不会再出现重复审批。本文还有配套的精品资源点击获取