用 50 行 YAML 实现一个请假审批流(含中断恢复、并行网关、条件分支) 一、为什么需要流程编排做后台开发的同学一定遇到过这类需求请假审批员工提交 - 主管审批 - 部门经理审批 - 副总审批但不同天数的审批链路还不一样合同审批金额超过 10 万需要额外部门会签超过 50 万需要财务参与数据抓取多线程并行爬取多个数据源再汇总处理这类需求本质上都是一个有向图的流转问题。如果全靠 if-else 硬编码代码很快就会变成面条式意大利粉。Solon Flow是 Solon 生态中的通用流程编排框架它的核心思路是用 YAML或 Java Fluent API描述流转逻辑用引擎驱动执行。不需要重依赖、不需要外部数据库、不需要部署服务一个 jar 包就能跑。今天这篇文章我们用最经典的请假审批流作为实战案例从零到一跑通 Solon Flow 的核心能力。二、5 分钟跑起来Hello World2.1 添加依赖在pom.xml中添加dependency groupIdorg.noear/groupId artifactIdsolon-flow/artifactId /dependency不需要额外数据库、不需要消息中间件一个依赖搞定。2.2 创建流程定义文件在resources/flow/目录下创建demo1.ymlid: 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开始节点---01activity活动节点缺省类型支持--1..n1..nexclusive排他网关单选支持支持-1..n1..ninclusive包容网关多选支持支持-1..n1..nparallel并行网关全选支持-支持1..n1..nloop循环网关支持--11end结束节点---1..n0几个关键点start和end每个图必须有且仅有一个exclusive排他网关相当于单选只有一条满足条件的连接会流出没有匹配的用默认无条件连接inclusive包容网关相当于多选所有满足条件的连接都会流出需要成对使用实现汇聚parallel并行网关相当于全选所有连接都会流出内部使用多线程执行需要成对使用实现汇聚loop循环网关遍历集合并循环流出通过$for和$in元数据指定变量3.2 流程引擎FlowEngineFlowEngine是执行流程的核心入口主要负责加载和解析流程定义支持 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 上下文FlowContextFlowContext是流程执行的核心现场承载了业务数据、控制状态和执行轨迹// 创建 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 实现请假审批流条件分支 exclusive4.1 业务规则一个请假审批的流转规则如下员工发起请假 - 主管审批 - 天数 3天直接结束主管批了就行 - 天数 3天部门经理审批 - 天数 7天结束 - 天数 7天副总审批 - 结束这是一个典型的条件分支场景用exclusive排他网关来实现。4.2 流程定义创建resources/flow/leave-approval.ymlid: 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: day3 } ]} - { 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: day7 } ]} - { id: n3, type: activity, title: 副总审批, task: vp_approve, link: e } - { id: e, type: end }一共 8 个节点含 start 和 endYAML 共50 行以内。解释几个关键点task: tl_approve以开头的是任务组件引用对应 Java 中注册的TaskComponentwhen: day3连接条件表达式引擎通过表达式引擎SnEL求值day从上下文中获取排他网关g1如果day3满足走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 }关键说明g1是并行网关的分叉端所有流出连接都会同时执行多线程g2是并行网关的汇聚端等待所有流入连接到齐后才继续往后流转parallel 必须成对使用分叉 汇聚任务组件Component(send_sms) public class SendSms implements TaskComponent { Override public void run(FlowContext context, Node node) throws Throwable { String applicant context.getAs(applicant); System.out.println([短信] 通知 applicant 您的请假申请已审批通过); } } Component(send_email) public class SendEmail implements TaskComponent { Override public void run(FlowContext context, Node node) throws Throwable { String applicant context.getAs(applicant); System.out.println([邮件] 通知 applicant 您的请假申请已审批通过); } }运行后短信和邮件两个任务会并行执行parallel 节点内部使用多线程执行完毕后在g2汇聚然后继续往后。六、实战三中断与恢复 — 审批到一半要等领导出差回来实际业务中审批往往不是一次完成的。比如副总出差了流程要暂停等副总回来再继续。这就是 Solon Flow 的中断与恢复能力。6.1 核心机制在任务组件中调用context.stop()流程会停在当前节点通过context.toJson()将整个上下文序列化为 JSON 快照需要恢复时通过FlowContext.fromJson()还原上下文再次调用engine.eval()引擎会自动从上次停止的节点继续执行6.2 修改副总审批组件加入中断逻辑Component(vp_approve) public class VPApprove implements TaskComponent { Override public void run(FlowContext context, Node node) throws Throwable { String applicant context.getAs(applicant); boolean vpAvailable context.getAs(vpAvailable); // 副总是否在岗 if (!vpAvailable) { System.out.println(副总出差中流程暂停等待。申请人 applicant); context.stop(); // 中断流程 return; } System.out.println(副总审批通过申请人 applicant); context.put(vp_approved, true); } }6.3 执行与恢复public class InterruptResumeTest { public static void main(String[] args) { FlowEngine engine FlowEngine.newInstance(); engine.load(classpath:flow/leave-approval.yml); // 第一阶段提交请假副总出差流程暂停 FlowContext context FlowContext.of(leave-001) .put(applicant, 王五) .put(day, 10) .put(vpAvailable, false); // 副总不在 engine.eval(leave-approval, context); // 输出主管审批通过... 部门经理审批通过... 副总出差中流程暂停等待 System.out.println(流程是否已停止: context.isStopped()); // true System.out.println(最后执行节点: context.lastNodeId()); // n3 // 持久化快照到数据库实际项目中 String snapshot context.toJson(); // db.save(leave-001, snapshot); // 第二阶段副总回来了恢复流程 // 从数据库加载快照 // String snapshot db.load(leave-001); FlowContext restored FlowContext.fromJson(snapshot); restored.put(vpAvailable, true); // 更新状态副总回来了 engine.eval(leave-approval, restored); // 输出副总审批通过申请人王五 } }这个能力非常强大。上下文的序列化和恢复是快照级别的意味着可以跨线程恢复异步回调场景可以跨时间恢复长流程场景可以跨服务器恢复分布式场景配合数据库持久化快照非常适合审批流、AI 对话流等长流程场景。七、实战四循环遍历 — 批量审批假设部门经理需要一次性审批团队所有待处理的请假单可以用loop循环网关实现id: batch-approve title: 批量审批 layout: - { id: s, type: start, link: g1 } - { id: g1, type: loop, meta: { $for: item, $in: pendingList }, link: n1 } - { id: n1, type: activity, title: 逐条审批, task: batch_item_approve, link: g2 } - { id: g2, type: loop, link: e } - { id: e, type: end }关键说明第一个loop节点g1是循环的流出端通过$in从上下文中获取集合pendingList用$for声明循环变量名item循环每取出一个元素推入上下文变量item后续节点可直接使用第二个loop节点g2是循环的汇聚端等待遍历结束后才往后流转loop 必须成对使用任务组件Component(batch_item_approve) public class BatchItemApprove implements TaskComponent { Override public void run(FlowContext context, Node node) throws Throwable { // 每次循环item 变量自动推入上下文 MapString, Object item context.getAs(item); System.out.println(批量审批处理 item.get(applicant) - item.get(day) 天); } }执行FlowContext context FlowContext.of() .put(pendingList, Arrays.asList( Map.of(applicant, 张三, day, 1), Map.of(applicant, 李四, day, 4), Map.of(applicant, 王五, day, 8) )); engine.eval(batch-approve, context); // 输出 // 批量审批处理张三 - 1天 // 批量审批处理李四 - 4天 // 批量审批处理王五 - 8天八、实战五Graph 编码模式 — 用 Java Fluent API 定义流程除了 YAMLSolon Flow 还支持用纯 Java 代码定义流程。这在需要动态构建流程、或者流程需要根据参数动态调整的场景下非常有用。import org.noear.solon.flow.core.Graph; Graph graph Graph.create(leave, spec - { spec.addStart(s).title(发起人).linkAdd(n1); spec.addActivity(n1).title(主管审批).task(tl_approve).linkAdd(g1); spec.addExclusive(g1) .linkAdd(e, l - l.title(3天以下)) .linkAdd(n2, l - l.title(3天以上).when(day3)); spec.addActivity(n2).title(部门经理审批).task(dm_approve).linkAdd(g2); spec.addExclusive(g2) .linkAdd(e, l - l.title(7天以下)) .linkAdd(n3, l - l.title(7天以上).when(day7)); spec.addActivity(n3).title(副总审批).task(vp_approve).linkAdd(e); spec.addEnd(e); }); // 直接执行 FlowEngine engine FlowEngine.newInstance(); engine.eval(graph);Fluent API 的方法名与 YAML 中的节点类型完全对应spec.addStart(id)/spec.addEnd(id)/spec.addActivity(id)/spec.addExclusive(id)....title()设置标题.task()设置任务.linkAdd(nextId)添加流出连接无参 lambda 可设置连接标题和条件.when()设置连接条件表达式此外Graph 还支持复制和修改已有图// 复制并修改为新图 Graph graphNew Graph.copy(graph, spec - { spec.removeNode(n3); // 移除副总审批节点 spec.getNode(n2).linkRemove(n3).linkAdd(e); // n2 直接连到 end });这种能力非常适合做流程模板先定义基础流程再根据租户/业务线动态裁剪。两种模式也可以互相转换// Graph - YAML反向序列化 String yaml graph.toYaml(); // Graph - JSON String json graph.toJson(); // Graph - PlantUML 状态图文本v3.9.5 String plantuml graph.toPlantuml();九、高级能力拦截器、事件总线、单步调试9.1 拦截器AOP 能力Solon Flow 提供了FlowInterceptor拦截器接口可以在不修改流程定义和任务组件的前提下对流程执行进行切面增强。常见的使用场景场景实现方式性能监控doIntercept中统计总耗时执行审计onNodeStart记录节点 ID 和上下文参数安全校验doIntercept中校验必要 Token参数补全流程开始前向上下文注入全局配置Component public class FlowAuditInterceptor implements FlowInterceptor { static Logger log LoggerFactory.getLogger(FlowAuditInterceptor.class); Override public void doIntercept(FlowInvocation inv) throws Throwable { long start System.currentTimeMillis(); try { inv.invoke(); // 执行流程 } catch (Throwable ex) { log.error(流程执行异常: inv.getStartNode().getGraph().getId(), ex); throw ex; } finally { long elapsed System.currentTimeMillis() - start; log.info(流程 {} 执行耗时: {}ms, inv.getStartNode().getGraph().getId(), elapsed); } } Override public void onNodeStart(FlowContext context, Node node) { log.info(节点开始: id{}, title{}, node.getId(), node.getTitle()); } Override public void onNodeEnd(FlowContext context, Node node) { log.info(节点完成: id{}, title{}, node.getId(), node.getTitle()); } }拦截器可以通过 Solon 容器自动注册Component也可以手动注册engine.addInterceptor(new FlowAuditInterceptor());9.2 事件总线解耦利器Solon Flow 内置了基于 DamiBus 的事件总线支持在流程执行中进行上下文级别实例级别的事件通信。三种模式模式方法说明发送send异步/同步广播不要求答复请求call发送并等待答复类似 RPC流stream发送并持续接收数据流在流程中触发事件Component(notify_task) public class NotifyTask implements TaskComponent { Override public void run(FlowContext context, Node node) throws Throwable { // 发送事件广播 context.eventBus().send(leave.approved, context.getAs(applicant)); } }在外部订阅事件FlowContext context FlowContext.of(); context.eventBus().Stringlisten(leave.approved, event - { System.out.println(收到审批通过通知: event.getPayload()); // 在这里处理发企微通知、更新统计报表、推送消息队列等 }); engine.eval(leave-approval, context);通过事件总线流程定义只管发信号具体的业务处理发通知、写日志、更新缓存由外部监听器处理实现了极度解耦。9.3 单步前进调试模式Solon Flow 支持深度控制可以逐步执行流程中的每一个节点类似调试器的单步执行FlowEngine engine FlowEngine.newInstance(); engine.load(classpath:flow/leave-approval.yml); FlowContext context FlowContext.of() .put(applicant, 张三) .put(day, 2); // 每次执行一步深度1 engine.eval(leave-approval, 1, context); System.out.println(当前节点: context.lastRecord().getTitle()); // 发起人 engine.eval(leave-approval, 1, context); System.out.println(当前节点: context.lastRecord().getTitle()); // 主管审批 engine.eval(leave-approval, 1, context); System.out.println(当前节点: context.lastRecord().getTitle()); // 天数判断这个能力非常适合流程调试开发阶段逐步排查问题流程演示向产品/业务逐步展示流转过程交互式流程每个节点执行后等待用户确认再继续十、可视化设计器对于不熟悉 YAML 的业务人员Solon Flow 提供了可视化设计器可以通过拖拽的方式设计流程图然后导出为 YAML/JSON。设计器特点拖拽式节点编排所见即所得支持 7 种节点类型的可视化配置可设置节点属性标题、任务、条件、元数据等导出为 YAML/JSON/Map 格式与代码模式无缝衔接有了设计器流程定义可以由业务人员直接参与维护开发人员只需要关注任务组件的实现实现真正的职责分离。十一、包容网关补充 — 条件会签场景除了排他网关单选和并行网关全选还有一种包容网关inclusive相当于多选——满足条件的连接都会流出。以合同审批为例金额不同需要不同部门会签id: contract-approval title: 合同审批条件会签 layout: - { id: s, type: start, title: 发起人, meta: { role: employee }, link: n1 } - { id: n1, type: activity, title: 主管批, meta: { role: tl }, link: g1 } - { id: g1, type: inclusive, title: 会签, link: [ { nextId: n2, title: 10万以上, when: amount100000 }, { nextId: n3, title: 50万以上, when: amount500000 }, { nextId: n4, title: 90万以上, when: amount900000 } ]} - { id: n2, type: activity, title: 本部门经理批, task: dm_approve, link: g2 } - { id: n3, type: activity, title: 生产部经理批, task: prod_approve, link: g2 } - { id: n4, type: activity, title: 财务部经理批, task: fin_approve, link: g2 } - { id: g2, type: inclusive, link: e } - { id: e, type: end }如果合同金额 80 万则g1会同时流出n210万以上和n350万以上两个部门并行会签。g2作为汇聚端等待所有满足条件的流入到齐后才继续。十二、总结与进阶12.1 本文覆盖的核心能力能力对应特性实战章节条件分支exclusive 排他网关实战一请假审批并行执行parallel 并行网关实战二并行通知中断恢复context.stop() toJson/fromJson实战三中断恢复循环遍历loop 循环节点 $for/$in实战四批量审批Fluent APIGraph.create() 硬编码实战五Graph 编码条件会签inclusive 包容网关十一章合同审批AOP 扩展FlowInterceptor 拦截器高级能力事件解耦DamiBus 事件总线高级能力单步调试eval(id, depth, context)高级能力可视化设计可视化设计器第十章12.2 Solon Flow 的适用场景无状态流计算编排、规则决策、数据抓取、AI Agent 工具调用链有状态流审批流、长流程、需要中断恢复的业务场景混合场景审批流 自动化任务编排的混合流程12.3 进阶方向如果你已经在项目中使用 Solon Flow还可以关注以下进阶主题solon-flow-workflow在 solon-flow 基础上封装了工作流服务提供任务认领claimTask、任务查找findTask、状态控制StateController、状态持久化StateRepository等开箱即用的工作流能力引擎驱动定制通过自定义FlowDriver实现不同的执行策略元数据扩展通过meta字段实现节点级别的自定义扩展权限、超时、重试等Solon AI Flow将 Solon Flow 与 AI 能力结合实现 ReAct Agent、AI 工具链等场景PlantUML 导出通过graph.toPlantuml()生成状态图文档方便技术评审