做后台开发的同学一定遇到过这类需求:
- 请假审批:员工提交 -> 主管审批 -> 部门经理审批 -> 副总审批,但不同天数的审批链路还不一样
- 合同审批:金额超过 10 万需要额外部门会签,超过 50 万需要财务参与
- 数据抓取:多线程并行爬取多个数据源,再汇总处理
这类需求本质上都是一个有向图的流转问题。如果全靠 if-else 硬编码,代码很快就会变成面条式意大利粉。
Solon Flow是 Solon 生态中的通用流程编排框架,它的核心思路是:用 YAML(或 Java Fluent API)描述流转逻辑,用引擎驱动执行。不需要重依赖、不需要外部数据库、不需要部署服务,一个 jar 包就能跑。
今天这篇文章,我们用最经典的"请假审批流"作为实战案例,从零到一跑通 Solon Flow 的核心能力。
二、5 分钟跑起来:Hello World
2.1 添加依赖
在pom.xml中添加:
<dependency> <groupId>org.noear</groupId> <artifactId>solon-flow</artifactId> </dependency>不需要额外数据库、不需要消息中间件,一个依赖搞定。
2.2 创建流程定义文件
在resources/flow/目录下创建demo1.yml:
id: "demo1" layout: - { id: "s", type: "start", link: "n1" } - { id: "n1", type: "activity", task: 'System.out.println("hello world!");', link: "e" } - { id: "e", type: "end" }这是一个最简单的三节点流程:开始 -> 执行打印 -> 结束。
2.3 执行
两种方式,任选其一:
方式一:原生 Java 模式(不需要启动 Solon 容器)
import org.noear.solon.flow.FlowEngine; public class Demo { public static void main(String[] args) { FlowEngine engine = FlowEngine.newInstance(); engine.load("classpath:flow/*"); engine.eval("demo1"); // 控制台输出:hello world! } }方式二:Solon 容器注解模式
先在应用配置中声明流程文件位置:
# app.yml solon.flow: - "classpath:flow/*.yml"然后在组件中注入引擎直接使用:
@Component public class DemoCom implements LifecycleBean { @Inject private FlowEngine flowEngine; @Override public void start() throws Throwable { flowEngine.eval("demo1"); } }运行后控制台输出:hello world!。5 分钟,一个流程就跑通了。
三、核心概念速览:7 种节点 + 引擎 + 上下文
在进入实战之前,先过一遍 Solon Flow 的核心概念。这些概念非常精简,但足够覆盖绝大多数编排场景。
3.1 七种节点类型
Solon Flow 的图由7 种节点构成:
| 节点类型 | 说明 | 执行任务 | 连接条件 | 多线程 | 可流入 | 可流出 |
|---|---|---|---|---|---|---|
| start | 开始节点 | - | - | - | 0 | 1 |
| activity | 活动节点(缺省类型) | 支持 | - | - | 1..n | 1..n |
| exclusive | 排他网关(单选) | 支持 | 支持 | - | 1..n | 1..n |
| inclusive | 包容网关(多选) | 支持 | 支持 | - | 1..n | 1..n |
| parallel | 并行网关(全选) | 支持 | - | 支持 | 1..n | 1..n |
| loop | 循环网关 | 支持 | - | - | 1 | 1 |
| end | 结束节点 | - | - | - | 1..n | 0 |
几个关键点:
- start和end每个图必须有且仅有一个
- exclusive(排他网关):相当于"单选",只有一条满足条件的连接会流出,没有匹配的用默认(无条件)连接
- inclusive(包容网关):相当于"多选",所有满足条件的连接都会流出,需要成对使用实现汇聚
- parallel(并行网关):相当于"全选",所有连接都会流出,内部使用多线程执行,需要成对使用实现汇聚
- loop(循环网关):遍历集合并循环流出,通过
$for和$in元数据指定变量
3.2 流程引擎(FlowEngine)
FlowEngine是执行流程的核心入口,主要负责:
- 加载和解析流程定义(支持 YAML、JSON、Java 硬编码)
- 驱动流程执行
- 管理拦截器链
FlowEngine engine = FlowEngine.newInstance(); engine.load("classpath:flow/*.yml"); // 加载 engine.eval("demo1"); // 执行(按 id 匹配) engine.eval("demo1", context); // 执行(带上下文) engine.eval("demo1", 1, context); // 执行(深度控制,单步调试)3.3 上下文(FlowContext)
FlowContext是流程执行的核心现场,承载了业务数据、控制状态和执行轨迹:
// 创建 FlowContext context = FlowContext.of(); FlowContext context = FlowContext.of("instance-001"); // 传递业务参数 context.put("day", 5); context.put("applicant", "张三"); // 获取参数 int day = context.getAs("day"); // 控制流程 context.stop(); // 停止(可用于中断恢复) context.interrupt(); // 中断当前分支 // 序列化与恢复 String snapshot = context.toJson(); FlowContext restored = FlowContext.fromJson(snapshot); // 执行轨迹 context.lastNodeId(); // 最后执行的节点 ID context.trace(); // 完整执行轨迹概念不多,但覆盖了编排、执行、控制、恢复四大核心能力。下面进入实战。
四、实战一:50 行 YAML 实现请假审批流(条件分支 exclusive)
4.1 业务规则
一个请假审批的流转规则如下:
员工发起请假 -> 主管审批 -> 天数 < 3天:直接结束(主管批了就行) -> 天数 >= 3天:部门经理审批 -> 天数 < 7天:结束 -> 天数 >= 7天:副总审批 -> 结束这是一个典型的条件分支场景,用exclusive(排他网关)来实现。
4.2 流程定义
创建resources/flow/leave-approval.yml:
id: "leave-approval" title: "请假审批" layout: - { id: "s", type: "start", title: "发起人", meta: { role: "employee" }, link: "n1" } - { id: "n1", type: "activity", title: "主管审批", task: "@tl_approve", link: "g1" } - { id: "g1", type: "exclusive", title: "天数判断", link: [ { nextId: "e", title: "3天以下" }, { nextId: "n2", title: "3天以上", when: "day>=3" } ]} - { id: "n2", type: "activity", title: "部门经理审批", task: "@dm_approve", link: "g2" } - { id: "g2", type: "exclusive", title: "天数判断", link: [ { nextId: "e", title: "7天以下" }, { nextId: "n3", title: "7天以上", when: "day>=7" } ]} - { id: "n3", type: "activity", title: "副总审批", task: "@vp_approve", link: "e" } - { id: "e", type: "end" }一共 8 个节点(含 start 和 end),YAML 共50 行以内。
解释几个关键点:
task: "@tl_approve":以@开头的是任务组件引用,对应 Java 中注册的TaskComponentwhen: "day>=3":连接条件表达式,引擎通过表达式引擎(SnEL)求值,day从上下文中获取- 排他网关
g1:如果day>=3满足,走n2;否则走默认连接e(没有 when 的连接就是默认) meta: { role: "employee" }:元数据,可以在拦截器或任务中读取,用于权限判断等
4.3 任务组件实现
每个task中的@xxx引用,对应一个 Java 组件:
import org.noear.solon.flow.core.TaskComponent; import org.noear.solon.flow.core.FlowContext; import org.noear.solon.flow.core.Node; // 主管审批 @Component("tl_approve") public class TeamLeaderApprove implements TaskComponent { @Override public void run(FlowContext context, Node node) throws Throwable { String applicant = context.getAs("applicant"); int day = context.getAs("day"); System.out.println("主管审批通过:申请人=" + applicant + ", 天数=" + day); context.put("tl_approved", true); } } // 部门经理审批 @Component("dm_approve") public class DeptManagerApprove implements TaskComponent { @Override public void run(FlowContext context, Node node) throws Throwable { String applicant = context.getAs("applicant"); System.out.println("部门经理审批通过:申请人=" + applicant); context.put("dm_approved", true); } } // 副总审批 @Component("vp_approve") public class VPApprove implements TaskComponent { @Override public void run(FlowContext context, Node node) throws Throwable { String applicant = context.getAs("applicant"); System.out.println("副总审批通过:申请人=" + applicant); context.put("vp_approved", true); } }4.4 测试运行
public class LeaveApprovalTest { public static void main(String[] args) { FlowEngine engine = FlowEngine.newInstance(); engine.load("classpath:flow/leave-approval.yml"); // 场景1:请假2天 -> 主管审批 -> 结束 FlowContext ctx1 = FlowContext.of() .put("applicant", "张三") .put("day", 2); engine.eval("leave-approval", ctx1); // 输出:主管审批通过:申请人=张三, 天数=2 // 场景2:请假5天 -> 主管 -> 部门经理 -> 结束 FlowContext ctx2 = FlowContext.of() .put("applicant", "李四") .put("day", 5); engine.eval("leave-approval", ctx2); // 输出:主管审批通过... 部门经理审批通过... // 场景3:请假10天 -> 主管 -> 部门经理 -> 副总 -> 结束 FlowContext ctx3 = FlowContext.of() .put("applicant", "王五") .put("day", 10); engine.eval("leave-approval", ctx3); // 输出:主管审批通过... 部门经理审批通过... 副总审批通过... } }这就是完整的请假审批流。50 行 YAML + 几个任务组件,流转逻辑一目了然。
五、实战二:并行网关 — 同时发送短信和邮件通知
审批通过后,需要同时给申请人和 HR 发送短信通知和邮件通知。这是一个典型的并行执行场景,用parallel(并行网关)来实现。
id: "notify-flow" title: "审批通知" layout: - { id: "s", type: "start", link: "n1" } - { id: "n1", type: "activity", title: "审批完成", task: "@after_approve", link: "g1" } - { id: "g1", type: "parallel", title: "并行发送", link: ["n2", "n3"] } - { id: "n2", type: "activity", title: "发送短信", task: "@send_sms", link: "g2" } - { id: "n3", type: "activity", title: "发送邮件", task: "@send_email", link: "g2" } - { id: "g2", type: "parallel", title: "汇聚结果", link: "e" } - { id: "e", type: "end" }关键说明: