尧图精选

ruflo轻量嵌入式规则流引擎:用配置驱动告别if-else

🕒 发布时间:2026/9/8 17:01:43 📁 来源:尧图网络
最近一直在折腾一个叫ruflo的小项目名字是我随手起的念起来像个 flow 的变体实际是 rule flow 的意思。这年头一说 flow大家脑子里全是各种重量级引擎什么审批流、任务编排、状态机听着就头大。ruflo 的定位完全不是那个方向它就是一个轻到不能再轻的嵌入式规则流引擎解决的是另一类问题大量 if-else 分支逻辑散落在业务代码里每次改规则都要发版、测试、上线效率低到让人想骂人。如果你也是写业务系统的大概率遇到过这种场景运营提了个新需求说优惠券的发放规则要调整你打开代码一看好家伙几十个 if 嵌套在那里动一行都要慎之又慎。ruflo 的思路很简单把规则和代码拆开。代码负责执行流程规则负责描述流程怎么走。规则放进可配置的 JSON 里流程是什么样一目了然改动也只是配置变更不用动代码。这篇文章我会把这套引擎拆开来讲从一个想法到跑通完整流程包括核心设计、代码实现、常见的坑以及我实际用下来的体会。不管你是想自己做一版类似的轮子还是想直接拿这个思路改造手上的老系统都应该能从这里找到可以参考的东西。1. 内容整体设计与思路拆解1.1 为什么业务规则会变成一团乱麻业务流程不可能永远简单刚开始就三五个条件写个 if-else 也就几句话。但业务是活的三个月后加了新渠道半年后又多了用户分层一年后规则数量翻了几倍代码开始迅速膨胀。更让人头疼的是规则之间还有组合关系比如会员等级大于 3 且近 30 天有购买记录或者邀请人数超过 5 人这种表达式一嵌套起来读代码的成本就会直线上升。我见过最夸张的一次某个系统的风控判断逻辑写了 400 多行全是 if-else 套 if-else里面还混着各种魔法数。没有人敢动那个方法谁能保证改动之后所有分支都还正常后来实在顶不住了我们决定做一个规则抽取的框架把所有分支逻辑按节点拆开流程用图的方式来表达节点的顺序和条件全部外置到配置里。这就是 ruflo 最原始的出发点。1.2 ruflo 的定位和边界做轮子前必须先搞清楚边界不然很容易陷入过度设计的泥潭。常见的工作流引擎比如 Flowable、Camunda主打的是复杂流程编排、人工审批、会签、驳回等功能有独立的数据库表、管理界面、流程版本管理依赖一大堆服务。这样的东西适合大规模团队但对于大多数中小型业务系统来说杀鸡用牛刀反而平白增加运维复杂度。ruflo 的定位非常明确嵌入式、轻量级、专注规则流转。它不解决人工审批的问题不解决消息队列的问题也不解决分布式事务的问题它只做一件事根据预定义的规则流程找到一条执行路径按顺序执行路径上的节点返回结果。所以它的核心抽象只有两个节点和连线。节点是执行单元连线是路径选择规则。整个引擎可以理解为一张有向图从一个起点进去按规则往下走直到某个终点或者没有下一个节点为止。这个定位让 ruflo 可以嵌入到任何 Java 服务里不依赖外部存储不额外起服务引入 jar 包就能跑。1.3 什么东西不该往 ruflo 里塞这里我想多说一句。规则流引擎适合的是条件明确、路径可列举的场景比如优惠计算、风险评分、审核流程中的自动校验环节。但它不适合用来做长周期的人工流程也不适合承载大量 IO 密集型的任务处理更不适合做数据清洗那一类批量计算。我见过有同事试图把一个大报表的生成逻辑也画成规则流结果流程有几十个节点每个节点都要查数据库跑一次要好几分钟维护起来比原来还痛苦。这就是典型的场景错配。规则流引擎的本质是决策编排不是任务调度你把数据处理那种重量级的活塞进来等于让一个决策者去搬砖方向就错了。2. 核心细节解析与实操要点2.1 节点类型到底该设计多少种很多人在设计流程引擎时有个误区觉得节点类型要丰富开始节点、结束节点、任务节点、条件节点、并行节点、子流程节点、事件节点列了一堆结果实现起来半死不活。ruflo 的节点类型我压到了五类足够覆盖 90% 以上的业务场景。第一类是开始节点标记流程入口一个流程定义里唯一存在没有实际业务动作。第二类是动作节点真正执行具体逻辑的地方比如调风控接口计算优惠金额这类节点需要绑定一个 handler由业务方实现。第三类是条件节点它不执行业务动作只是做判断根据表达式结果走不同的分支。第四类是结束节点标记流程结束可以有多个。第五类是子流程节点用于把一段重复流程抽出来复用避免定义文件无限膨胀。这个设计有一个明显的好处业务方几乎只需要关心动作节点的 handler 编写其他节点都是配置层面的东西。你想想如果每个节点都要写一堆扩展代码那规则外置的意义就打了折扣。2.2 条件表达式的选型和设计条件节点是整张流程图的灵魂它负责分流。表达式该怎么写、用什么语法直接决定使用者体验。ruflo 用的是 SpEL也就是 Spring 的表达式语言选择它有三个原因第一Java 生态里做规则引擎的基本都会先用 SpEL 做原型验证第二SpEL 支持属性访问、方法调用、逻辑运算、三元表达式表达能力足够强第三它天然支持类型解析和空值安全。实际使用中条件表达式长这样#user.level 3 and #user.recentOrderCount 0或者#riskScore 60 or #blackList false。这里的#user、#riskScore是流程上下文里的变量由入口调用方注入。流程走到条件节点时引擎会用这些变量计算表达式拿到 true 或者 false然后选择对应的分支走。这里要注意一个非常实际的坑SpEL 表达式在写错属性名时默认会抛异常这其实是好事。但我见过有些团队的实现把表达式包在 try-catch 里当成 false 处理静默吞掉错误这会导致流程跑了很长的链路之后才发现某个条件判断一直在走错误的分支排查起来非常痛苦。规则引擎里条件表达式解析失败必须快速失败、直接抛出异常宁可让流程亮红也不要让错误悄悄蔓延。2.3 执行引擎的核心逻辑和上下文设计流程执行的过程本质上是一个图遍历的过程。从开始节点出发沿着有向边往下走碰到条件节点就计算表达式选择一条边继续碰到动作节点就执行 handler直到走到结束节点。这个逻辑听起来简单但实现上有一个容易翻车的点流程上下文Context的生命周期。ruflo 的上下文是一个 ObjectNode本质上是一个 JSON 结构体可以嵌套、可以动态增加字段。所有节点之间通过上下文来传递数据。比如风控节点执行后往上下文里放了一个riskScore后续条件节点就能读取它来做判断。这样的设计有一个优势节点之间完全解耦动作节点不需要知道上游是谁只要保证产出字段的命名约定清晰即可。不过字段命名约定这种东西靠人盯是盯不住的。我的建议是给每个动作节点的产出字段做一个集中管理至少列一个字段清单写清楚哪个节点会产生哪个字段、哪些节点会消费哪个字段。虽然听起来很原始但在中小团队里这个清单比什么自动化文档都管用。2.4 错误处理机制和重试策略流程跑起来不可能永远一帆风顺。外部接口超时了、数据库连接断了、handler 里代码抛异常了这些都是常态。ruflo 的错误处理机制我设计了三层。第一层是节点级容错。每个动作节点可以配置一个 errorHandler 指向另一个节点当节点执行失败时流程不会立即终止而是跳到 errorHandler 表示的处理逻辑比如发一个告警或者走降级分支。这个设计非常实用特别是依赖外部系统时不用把整个流程都拖垮。第二层是流程级兜底。如果整个流程执行到最后仍然是失败状态引擎会抛出统一的 FlowExecutionException由业务方决定如何响应。第三层是重试。ruflo 支持对指定节点配置重试次数和重试间隔适用于临时性故障比如网络抖动。但这里我强烈建议重试一定要做退避不要无脑立即重试否则对接的下游服务可能在抖动恢复前被你的重试请求打崩。我见过太多因为重试策略不当引发的线上事故重试本身是好的但要克制。3. 实操过程与核心环节实现3.1 初始化引擎和编写流程定义先看怎么把 ruflo 跑起来。第一步自然是引入依赖把 jar 包放到工程里然后初始化引擎。RuleEngine engine RuleEngineBuilder.create() .registerHandler(riskCheck, new RiskCheckHandler()) .registerHandler(calcDiscount, new CalcDiscountHandler()) .registerHandler(sendNotify, new SendNotifyHandler()) .build();这里的 registerHandler 就是把 handler 绑定到一个名字上流程定义里通过这个名字来引用对应的实现。handler 接口本身很简单只有一个 execute 方法入参是流程上下文返回值是节点执行结果。接下来是流程定义。因为整个引擎设计的核心是配置驱动所以流程定义是 JSON 文件放到 classpath 下即可。我拿一个电商下单的场景来举例流程包含风控检查、优惠计算、库存校验、入库通知这几个节点。{ flowId: order-submit, version: 35, startNode: start, nodes: [ { id: start, type: start, next: riskCheck }, { id: riskCheck, type: action, handler: riskCheck, next: riskDecide }, { id: riskDecide, type: condition, expression: #riskPassed true, whenTrue: calcDiscount, whenFalse: endReject }, { id: calcDiscount, type: action, handler: calcDiscount, next: stockCheck }, { id: stockCheck, type: action, handler: stockCheck, next: stockDecide }, { id: stockDecide, type: condition, expression: #stockEnough true, whenTrue: sendNotify, whenFalse: endReject }, { id: sendNotify, type: action, handler: sendNotify, next: endSuccess }, { id: endSuccess, type: end, name: 下单成功 }, { id: endReject, type: end, name: 下单失败 } ] }这个流程已经能说明问题了有两个条件节点有两个结束节点路径完全由配置驱动。以后如果业务要求风控不通过但金额小于 100 元也能下单只需要增加一个条件分支改配置就行一行 Java 代码都不用动。3.2 动作节点的实现和上下文数据流转看一个 handler 的实际写法。以风控检查节点为例它可能做了很多事情从上下文里拿订单信息调外部风控系统把风险分写入上下文。public class RiskCheckHandler implements FlowHandler { Override public HandlerResult execute(FlowContext context) { String orderId context.getString(orderId); long userId context.getLong(userId); int riskScore riskClient.evaluate(orderId, userId); context.set(riskScore, riskScore); context.set(riskPassed, riskScore 60); return HandlerResult.success(); } }你看节点之间只通过上下文交互依赖关系全部是隐式的。这也带来一个问题如果某个节点忘了写某个字段下游条件节点一执行就会报错定位起来稍微费点劲。这部分我在后面的常见问题里会讲排查方法。3.3 条件表达式的运行机制再来看看条件节点具体是怎么工作的。引擎执行到条件节点时会把流程上下文中的所有字段展开成表达式变量。举个例子如果上下文里有riskScore: 45和riskPassed: true那么表达式#riskPassed true就能直接访问到这两个变量。这里有一个很重要的细节条件节点只允许两个分支true 走一边false 走另一边。有人可能会问多分支怎么处理我的方案是级联条件节点。比如要根据用户会员等级分三条路可以先用一个条件判断等级大于 3true 走 A 流程false 再进入下一个条件节点判断等级等于 2以此类推。这种设计虽然配置上会多一点节点但是逻辑非常清晰排查问题的时候一眼能看穿整个链路。SpEL 表达式对空值比较敏感。比如表达式#user.level 3如果user字段为空SpEL 不会返回 null 而是直接抛异常。这是好事能帮你尽早暴露上下文缺失的问题。不过如果你确实希望某些场景跳过判断可以在表达式里显式加空值检查比如#user null or #user.level 3语义更清晰。3.4 执行入口和结果返回流程定义和 handler 都就绪之后调用入口非常简单FlowContext context FlowContext.create() .set(orderId, orderId) .set(userId, userId) .set(orderAmount, amount) .set(sku, skuId); FlowResult result engine.execute(order-submit, context);execute 方法的逻辑是加载流程定义校验 DAG 性从 start 节点开始遍历依次执行节点逻辑直到到达结束节点。执行完之后FlowResult 里会包含最终状态成功或者失败、经过的节点列表、每个节点的耗时、以及最终上下文快照。FlowResult 里经过的节点列表特别有用。上线初期我习惯把每个单子的流程路径打到日志里出现问题时直接从日志里看跑了哪条分支比对配置是否符合预期定位速度比从前翻 if-else 快了太多。3.5 动态更新流程定义流程定义放在 classpath 里意味着每次修改配置要重新发版这个在一定程度上违背了快速调整规则的初衷。ruflo 对这个问题的解法很简单把 JSON 定义放进配置中心或者数据库引擎启动时加载并提供 refresh 接口手动刷新。刷新操作看起来很美好但要处理好版本兼容的问题。ruflo 在流程定义里加了一个 version 字段每次变更版本号加一。引擎执行时不会强制要求版本匹配但会在 Result 里记录版本号。这样如果新版配置有问题至少能从日志上快速定位到当前跑的是哪个版本的流程回滚的时候也知道该回滚到哪个版本。另外刷新接口一定要加权限控制。规则配置直接影响业务流程如果任何人都能改线上流程定义那离事故就不远了。不管对接的是配置中心还是自研的管理后台权限验证都省不了。4. 常见问题与排查技巧实录4.1 流程走完了但结果不符合预期这是使用规则流引擎后最常遇到的问题。排查思路其实很固定先从 FlowResult 里拿完整路径看实际走了哪些节点。然后逐个检查条件节点上下文里参与计算的那个字段值到底是什么。很多时候不是流程画错了而是上游节点产出的字段名和条件节点引用字段名对不上比如一个叫riskPassed一个叫riskPassSpEL 解析时拿不到值直接抛异常反而还好定位最怕的是字段名碰巧都叫risk但一个是字符串一个是布尔表达式结果永远和你预期不一致。我现在的做法是条件节点里只要引用了某个字段就约定必须在 node 定义里显式声明一下依赖可以在定义文件里加一个dependencies字段。这样在流程加载阶段就能做一个静态校验如果上游没有任何节点会产出这个字段直接加载失败从源头避免问题。4.2 流程出现死循环ruflo 允许流程定义成任意有向图但执行时如果图里有环就可能出现死循环。我在设计初始就从规则上禁止了循环流程图加载后会自动做环检测检测到环直接抛异常。不过环检测拦不住一种隐性死循环条件表达式永远为 true导致节点虽只有一步但通过错误处理逻辑反复跳回来。比如 errorHandler 指回自己失败之后重试又失败又跳到 errorHandler形成无限循环。这个通过环检测发现不了因为图的层面它不是一个环。我的应对策略是给整个流程的执行节点数设一个硬上限默认 200 个。超过这个数量引擎直接抛异常终止执行。这是一个保命设计宁可流程失败也不要让线上服务卡死在线程里。4.3 并发执行时上下文串了这个问题踩过坑才意识到。ruflo 的流程上下文是执行实例级别的我最初用了 ThreadLocal 来传递上下文结果在高并发场景下线程池复用线程导致上下文污染A 请求的数据串到了 B 请求上排查起来极其痛苦。后来改了设计流程上下文作为方法参数显式传递不存放在 ThreadLocal 里。所有 handler 的 execute 方法签名都带 FlowContext 参数后续节点从参数拿数据。这是一个很基础的设计变更但是副作用巨大顺带解决了异步链路上下文丢失的问题。规则流引擎这类组件宁可传参传得啰嗦也不要用隐式的线程局部变量。4.4 流程配置变更发布之后老请求还在跑线上配置更新时可能有一些请求已经在流程执行中途了。如果配置改得比较激进比如删掉了某个节点正在执行的请求可能会找不到下一个节点而直接报错。我的做法是加载流程定义时生成一个不可变的对象引擎执行时引用的是这个不可变对象而不是每次执行时现读配置。这样即使外部配置变了已经开始的流程实例仍然跑在旧版本的定义上不会受影响。新请求进来才会用新定义。这个方案实现上只是快照机制但确实能避免线上大量执行中途失败的报警。4.5 性能和业务耦合的权衡最后说一句性能和业务架构的关系。ruflo 本身的图遍历开销非常小单次流程的执行耗时基本等于所有节点 handler 的耗时之和。真正的性能瓶颈从来不在引擎而在 handler 里干了什么重活。我建议动作节点尽量保持轻逻辑只做计算、调用、写库不要把一个批量任务塞进节点里。如果你在计划让这个节点去处理几万条数据或者去调一个响应耗时超过十秒的外部接口那应该考虑的是把任务提交到异步队列而不是在规则流节点里同步等待。规则流毕竟是一个决策编排器它擅长快速走完一条路径并返回结果而不是长时间驻留执行。5. 一点实际使用的体会ruflo 这个名字我后来越看越喜欢简单好读别人问起来我就说 rule flow一句话能解释清楚定位。做这个小项目最大的收获倒不是技术上的而是让我想明白了一件事很多系统之所以难维护不是代码写得差而是决策逻辑和业务逻辑混在了一起。把人脑中的规则抽象成可配置的图代码的核心逻辑反而变得非常简单后续加需求改规则基本就是改配置的事。在实际使用中我更倾向于把它用在那些最多变的环节上——营销活动规则、运营审核条件、复杂计算的路径选择。固定不变的流程我更愿意直接写在代码里这也是我反复提到的边界问题。规则流不是银弹但它确实能帮你把最脆弱、最容易变的那部分逻辑从代码里剥离出来让你在业务方频繁提需求的时候稳住自己的心态。如果你正在做的系统也有类似的问题我建议你从小处入手不要一上来就搞一个全功能平台。先抽一个场景把最乱的一段分支逻辑改成配置驱动跑通之后让业务方体验一下改规则不用发版的爽感再逐步推广。事情总要一步步来工具也一样。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →