LiteFlow规则引擎实战博客-2026-09-09

LiteFlow 规则引擎实战:把"资格判分"从 500 行 if/else 重构成可编排组件链

阅读对象:后端开发 / 技术负责人/以及感兴趣的读者

主题:LiteFlow 在真实项目中的应用------从"该不该用、怎么选型"到"核心概念、落地套路、坑与优化"

技术栈:Java 8 / Spring Boot / LiteFlow 2.16

说明:为保护业务,文中业务均为基础示意、代码为伪代码;机制与实现思路取自真实实践。


0. TL;DR(先给结论)

  • 当一段业务是"固定流程 + 少量分支"(一堆校验依次跑、不过就停、过了才算结果)→ 用编排引擎,而不是专家规则引擎(Drools)。
  • LiteFlow 是轻量、Spring 原生、流程可读(flow.xml + EL)、组件即 Bean 的编排框架,最贴合这类场景。
  • 三个必须掌握的内核:THEN 链、Context 贯穿、setIsEnd 短路;一个关键设计:判分与执行分离(先算账写状态,后异步真正执行)
  • 坑要早知道:组件名强绑定、执行日志量大、规则不热更、Context 膨胀、异步可靠性与兜底。

1. 背景:一个"资格判分"场景怎么把代码写崩的

电商/汽车等领域常见这类业务:用户在一个活动里完成若干行为,系统按"规则"判定够不够格、该发多少奖励

典型规则(示意):

行为 参与资格 发放金额 备注
注册 非员工、非黑名单 10 需 30 天内
预约 注册满 3 天、非重复 20 位置与门店 ≥5km 才发
下单 绑定手机、非重复 50 30 天内
履约 下单满 3 天、当月≤3 次 100 需门店核销

再加运营要"随时改规则按活动开关节点"。于是第一版代码长这样(伪代码):

java 复制代码
if (notEmployee(user) && notBlack(user)) {
    if (registerWithin30d(user)) {
        if (checkDealerDistance(lat1,lng1,lat2,lng2) >= 5) { point += 10; }
    }
    if (registeredDays >= 3 && !duplicate(order)) { ... }
    if (bindValid(user, order) && !duplicate(order)) { ... }
    if (monthlyCount(user) < 3) { ... }
} else { setNotQualified(user); }

问题很现实(表格):

痛点 表现
不可配置 改一条规则 = 改代码 + 发版
顺序敏感 想调一个校验顺序,牵一发动全身
扩展难 加一个校验 = 在主流程里再插一个 if,越来越长
分支爆炸 4 个行为 × N 校验,if/else 组合爆炸
不可测 链上没有独立的"单点校验",只能整链路黑盒测

这是明显的"流程编排"问题,不是"规则推理"问题。 于是引入编排框架。


2. 选型:为什么是 LiteFlow

2.1 先分清两个世界

  • 规则推理(Drools/Rete):适合"大量规则、规则可叠加、由事实推导结论",代价是内存/Rete 网络、学习成本、DRL 与 Java 调试断层。
  • 流程编排(LiteFlow 等):适合"节点固定有序、任一步不过即停、节点内做计算",就是上面这个场景。

我们这是编排,不是推理,所以不选 Drools。

2.2 对比表(业界常见选择)

维度 LiteFlow Drools Easy Rules URule Pro 手写责任链/策略
类型 流程编排框架 规则专家系统(Rete) 轻量规则引擎 可视化规则引擎(决策表/决策树) 自己造轮子
复杂度落点 组件化编排 海量规则推理 简单条件规则 业务人员可视化配置 无框架开销但自维护
Spring 集成 原生(组件即 Bean) 需适配 提供 starter,集成简单 提供集成,偏重量 天然能用但要写骨架
学习成本 低(flow.xml+EL) 高(DRL 规则语言) 很低(注解/API) 中(规则编辑器+权限治理)
流程可读性 高(XML 声明式,链式) 低(规则散落) 中(Rules 集合平铺) 高(可视化) 中(代码组织)
短路/上下文/并行 内置(setIsEnd/Context/WHEN) 无编排语义 支持 Rule 条件短路 部分(依赖规则编排) 自己实现
适用场景 固定流程+分支,需多节点串联编排 大量可叠加规则、冲突消解 简单条件判断、轻度规则 运营/业务频繁改规则且要可视化 简单且不常变
性能/内存 轻量 Rete 内存重 轻量(规则少时) 中等(引擎较完整) 无框架开销
规则热更新 XML(可扩展从配置中心加载) 支持但复杂 有限 内置决策包热加载

补充:还有一类表达式引擎(Aviator、QLExpress 等),它们擅长"单条表达式求值",不解决"多节点流程编排",与本场景(需要串联十来个校验节点)不在同一层,故不列入主表。

2.3 一句结论

判分是"在编排 ",不是"在推理 "------所以选轻量编排框架而不是规则专家系统;在 LiteFlow / Drools / Easy Rules / URule 中,LiteFlow 是"Spring 原生 + 节点可串联可短路 + 流程可读"最贴合本场景的那个。

一句话记法:

  • 可视化给运营配规则 → URule;
  • 就是几条简单条件 → Easy Rules;
  • 海量可叠加规则、要冲突消解 → Drools;
  • 固定流程 + 多节点可插拔校验/计算 → LiteFlow。

2.4 依赖与配置(架子)

xml 复制代码
<dependency>
    <groupId>com.yomahub</groupId>
    <artifactId>liteflow-spring-boot-starter</artifactId>
    <version>2.16.0</version>
</dependency>
yaml 复制代码
liteflow:
  rule-source: liteflow/flow.el.xml   # 流程定义文件
  print-execution-log: true           # 执行日志(注意生产日志量)

3. 核心概念:Chain / Node / Context / Slot

一张图说清骨架(Mermaid):
#mermaid-svg-pOBk05bg86hmuTdQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pOBk05bg86hmuTdQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pOBk05bg86hmuTdQ .error-icon{fill:#552222;}#mermaid-svg-pOBk05bg86hmuTdQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pOBk05bg86hmuTdQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pOBk05bg86hmuTdQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pOBk05bg86hmuTdQ .marker.cross{stroke:#333333;}#mermaid-svg-pOBk05bg86hmuTdQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pOBk05bg86hmuTdQ p{margin:0;}#mermaid-svg-pOBk05bg86hmuTdQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pOBk05bg86hmuTdQ .cluster-label text{fill:#333;}#mermaid-svg-pOBk05bg86hmuTdQ .cluster-label span{color:#333;}#mermaid-svg-pOBk05bg86hmuTdQ .cluster-label span p{background-color:transparent;}#mermaid-svg-pOBk05bg86hmuTdQ .label text,#mermaid-svg-pOBk05bg86hmuTdQ span{fill:#333;color:#333;}#mermaid-svg-pOBk05bg86hmuTdQ .node rect,#mermaid-svg-pOBk05bg86hmuTdQ .node circle,#mermaid-svg-pOBk05bg86hmuTdQ .node ellipse,#mermaid-svg-pOBk05bg86hmuTdQ .node polygon,#mermaid-svg-pOBk05bg86hmuTdQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pOBk05bg86hmuTdQ .rough-node .label text,#mermaid-svg-pOBk05bg86hmuTdQ .node .label text,#mermaid-svg-pOBk05bg86hmuTdQ .image-shape .label,#mermaid-svg-pOBk05bg86hmuTdQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-pOBk05bg86hmuTdQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pOBk05bg86hmuTdQ .rough-node .label,#mermaid-svg-pOBk05bg86hmuTdQ .node .label,#mermaid-svg-pOBk05bg86hmuTdQ .image-shape .label,#mermaid-svg-pOBk05bg86hmuTdQ .icon-shape .label{text-align:center;}#mermaid-svg-pOBk05bg86hmuTdQ .node.clickable{cursor:pointer;}#mermaid-svg-pOBk05bg86hmuTdQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pOBk05bg86hmuTdQ .arrowheadPath{fill:#333333;}#mermaid-svg-pOBk05bg86hmuTdQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pOBk05bg86hmuTdQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pOBk05bg86hmuTdQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pOBk05bg86hmuTdQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pOBk05bg86hmuTdQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pOBk05bg86hmuTdQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pOBk05bg86hmuTdQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pOBk05bg86hmuTdQ .cluster text{fill:#333;}#mermaid-svg-pOBk05bg86hmuTdQ .cluster span{color:#333;}#mermaid-svg-pOBk05bg86hmuTdQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pOBk05bg86hmuTdQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pOBk05bg86hmuTdQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-pOBk05bg86hmuTdQ .icon-shape,#mermaid-svg-pOBk05bg86hmuTdQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pOBk05bg86hmuTdQ .icon-shape p,#mermaid-svg-pOBk05bg86hmuTdQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pOBk05bg86hmuTdQ .icon-shape .label rect,#mermaid-svg-pOBk05bg86hmuTdQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pOBk05bg86hmuTdQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pOBk05bg86hmuTdQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pOBk05bg86hmuTdQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Context 传递数据
执行
Chain(一条链 = 一段业务判定)
setIsEnd=true 短路
通过
setIsEnd
读写
读写
读写
写结果
Node(key=registerParse)
Node(key=checkBlack)
Node(key=checkPeriod)
流程终止

写 NOT_QUALIFIED+原因
Node(key=rewardCalc)
END
flowExecutor.execute2Resp(chainName, null, context)
ActivityContext

(record/规则配置/计算结果)

  • Chainflow.xml 里一段 THEN(n1,n2,...),表示串行执行的节点序列。
  • Node(NodeComponent) :每个校验/计算是一个组件,@Component("xxx") 的 Bean 名要与链中 key 一致。
  • Context :跨组件共享的数据对象(不依赖静态变量),组件用 getContextBean(Context.class) 取。
  • Slot (理解即可):LiteFlow 内部的数据槽,execute2Resp(chain, slotIndex, context) 传入的 context 会被放入 slot 贯穿整链。

4. 落地:把"资格判分"重构成组件链

4.1 流程定义(伪代码,flow.el.xml 示意)

xml 复制代码
<flow>
    <!-- 注册行为判定链 -->
    <chain name="qualifyRegisterChain">
        THEN(registerParseCmp, checkPeriodCmp, checkBlack,
             recordTypeCount, locationCheckCmp, innerEmployeeCmp,
             accountLimitCmp, rewardCalcCmp, specialDateCmp, overlayCmp);
    </chain>
    <!-- 预约/下单:多了绑定时效与重复判断 -->
    <chain name="qualifyAppointmentChain">
        THEN(appointmentParseCmp, checkPeriodCmp, checkBlack,
             recordTypeCount, locationCheckCmp, innerEmployeeCmp,
             repeatCmp, bindValidityCmp, accountLimitCmp,
             rewardCalcCmp, specialDateCmp, overlayCmp);
    </chain>
    <!-- 履约:加锁定订单时效 + 月限 -->
    <chain name="qualifyFulfillmentChain">
        THEN(fulfillmentParseCmp, checkPeriodCmp, checkBlack,
             recordTypeCount, locationCheckCmp, innerEmployeeCmp,
             repeatCmp, bindValidityCmp, lockOrderValidityCmp,
             accountLimitCmp, rewardCalcCmp, specialDateCmp, overlayCmp);
    </chain>
</flow>

4.2 组件职责表(示意)

组件 key 职责 短路条件
xxParseCmp 解析该行为配置,填 Context;未开启/无配置则短路 无配置 / 节点未启用
checkPeriodCmp 有效期内判定 不在有效期
checkBlack 黑名单(比如手机号分维度) 命中黑名单
recordTypeCount 该用户在该行为下已发次数 超次数
locationCheckCmp 位置距离校验(Haversine) 距离不足
innerEmployeeCmp 内部员工不发 是员工
repeatCmp / bindValidityCmp / lockOrderValidityCmp 重复开关 / 绑定时间差 / 锁单时效 不满足
accountLimitCmp 月限 / 账限 超限
rewardCalcCmp 算分并写 PENDING(末位 ---
specialDateCmp / overlayCmp 特殊日期 / 白名单叠加奖励(追加记录) 未命中

4.3 关键机制一:setIsEnd 短路 vs 抛异常(重点)

语义 手段 后果
业务拒发(校验不过) this.setIsEnd(true) + 写 record.status=NOT_QUALIFIED + failReason 链终止、后续不执行,但数据正常保存(用户可看到"为什么没拿到")
系统故障(DB/Feign 异常) 组件抛 RuntimeException execute2Resp 返回 isSuccess=false,外层重新抛出 → 整个事务回滚

这就是"业务失败与系统失败分开建模",千万别把拒发当异常回滚。
#mermaid-svg-vE4OMyWx9EfKgJ6b{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vE4OMyWx9EfKgJ6b .error-icon{fill:#552222;}#mermaid-svg-vE4OMyWx9EfKgJ6b .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vE4OMyWx9EfKgJ6b .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vE4OMyWx9EfKgJ6b .marker.cross{stroke:#333333;}#mermaid-svg-vE4OMyWx9EfKgJ6b svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vE4OMyWx9EfKgJ6b p{margin:0;}#mermaid-svg-vE4OMyWx9EfKgJ6b defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-vE4OMyWx9EfKgJ6b g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-vE4OMyWx9EfKgJ6b g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-vE4OMyWx9EfKgJ6b g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-vE4OMyWx9EfKgJ6b g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-vE4OMyWx9EfKgJ6b g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-vE4OMyWx9EfKgJ6b .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-vE4OMyWx9EfKgJ6b .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vE4OMyWx9EfKgJ6b .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vE4OMyWx9EfKgJ6b .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vE4OMyWx9EfKgJ6b .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vE4OMyWx9EfKgJ6b .edgeLabel .label text{fill:#333;}#mermaid-svg-vE4OMyWx9EfKgJ6b .label div .edgeLabel{color:#333;}#mermaid-svg-vE4OMyWx9EfKgJ6b .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-vE4OMyWx9EfKgJ6b .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-vE4OMyWx9EfKgJ6b .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-vE4OMyWx9EfKgJ6b .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-vE4OMyWx9EfKgJ6b .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-vE4OMyWx9EfKgJ6b .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vE4OMyWx9EfKgJ6b #statediagram-barbEnd{fill:#333333;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .cluster-label,#mermaid-svg-vE4OMyWx9EfKgJ6b .nodeLabel{color:#131300;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-vE4OMyWx9EfKgJ6b .note-edge{stroke-dasharray:5;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-note text{fill:black;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram-note .nodeLabel{color:black;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagram .edgeLabel{color:red;}#mermaid-svg-vE4OMyWx9EfKgJ6b #dependencyStart,#mermaid-svg-vE4OMyWx9EfKgJ6b #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-vE4OMyWx9EfKgJ6b .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vE4OMyWx9EfKgJ6b :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
依次执行组件
校验通过
setIsEnd(true) 业务拒发
记录原因, 正常保存
算分
写状态待发
组件抛异常(系统故障)
外层事务回滚
NODE_A
CHECK
PASS
STOP
NEXT
SAVED
REWARD
PENDING
EX
ROLLBACK

4.4 核心伪代码(看懂即会用)

Context(跨组件共享):

java 复制代码
@Data
public class ActivityContext {
    private QualificationRecord record;      // 判定对象:结果直接写回其状态+原因
    private ActivityConfig config;           // 四类行为配置(JSON→对象)
    // ... 各组件间的缓存字段:黑白名单、距离、已发次数等
}

首个组件:解析配置填充 Context

java 复制代码
@Component("registerParseCmp")
public class RegisterParseCmp extends NodeComponent {
    @Override
    public void process() {
        ActivityContext ctx = getContextBean(ActivityContext.class);
        RegisterSetting s = parse(ctx.getConfig().getRegisterSetting());
        if (s == null || !rewardNodesEnabled(ctx, "REGISTER")) {
            ctx.getRecord().setStatus(NOT_QUALIFIED);   // 拒发
            ctx.getRecord().setReason("未配置注册奖励");
            setIsEnd(true);                              // 短路
            return;
        }
        ctx.setRegister(s);                              // 后续组件直接读
        ctx.getRecord().setRewardSetting(config.getRegisterSetting());
    }
}

末位组件:算分发 PENDING(只有前面的校验都过了才到这)

java 复制代码
@Component("rewardCalcCmp")
public class RewardCalcCmp extends NodeComponent {
    @Resource private RewardHelper helper;
    @Override
    public void process() {
        ActivityContext ctx = getContextBean(ActivityContext.class);
        if (NOT_QUALIFIED.equals(ctx.getRecord().getStatus())) return; // 别覆盖已拒发
        helper.calculateAndSetReward(ctx);   // 写 PENDING + points
    }
}

入口(触发一次判定)

java 复制代码
String chainName = resolveChain(record.getBehaviorType());  // 注册→qualifyRegisterChain ...
LiteflowResponse resp = flowExecutor.execute2Resp(chainName, null, context);
if (!resp.isSuccess()) {                    // 系统异常 → 回滚
    throw new RuntimeException(resp.getCause().getMessage());
}
// 判分完成:record 已被各组件写成 PENDING / NOT_QUALIFIED + 原因

5. 与 Spring 深度集成 + 异步解耦

5.1 组件即 Bean(可直接注入)

java 复制代码
@Component("specialDateCmp")
public class SpecialDateCmp extends NodeComponent {
    @Resource private SpecialDateService specialDateService; // 正常注入
    @Override public void process() { /* ... */ }
}

配合框架事务,组件里既有 Spring 能力、又保持"单一判定职责"。

5.2 判分与执行分离(重要设计)

判分链只算账写 PENDING ;真正执行(发钱/发分/扣库存)放事务提交后异步,避免长事务与跨域失败回滚扩散:
point/账户服务 事件/异步 record(DB) FlowExecutor 业务入口 point/账户服务 事件/异步 record(DB) FlowExecutor 业务入口 #mermaid-svg-ti8v2rjLPtrbOQf7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ti8v2rjLPtrbOQf7 .error-icon{fill:#552222;}#mermaid-svg-ti8v2rjLPtrbOQf7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ti8v2rjLPtrbOQf7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ti8v2rjLPtrbOQf7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ti8v2rjLPtrbOQf7 .marker.cross{stroke:#333333;}#mermaid-svg-ti8v2rjLPtrbOQf7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ti8v2rjLPtrbOQf7 p{margin:0;}#mermaid-svg-ti8v2rjLPtrbOQf7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ti8v2rjLPtrbOQf7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ti8v2rjLPtrbOQf7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ti8v2rjLPtrbOQf7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ti8v2rjLPtrbOQf7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ti8v2rjLPtrbOQf7 .sequenceNumber{fill:white;}#mermaid-svg-ti8v2rjLPtrbOQf7 #sequencenumber{fill:#333;}#mermaid-svg-ti8v2rjLPtrbOQf7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ti8v2rjLPtrbOQf7 .messageText{fill:#333;stroke:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ti8v2rjLPtrbOQf7 .labelText,#mermaid-svg-ti8v2rjLPtrbOQf7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .loopText,#mermaid-svg-ti8v2rjLPtrbOQf7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ti8v2rjLPtrbOQf7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ti8v2rjLPtrbOQf7 .noteText,#mermaid-svg-ti8v2rjLPtrbOQf7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ti8v2rjLPtrbOQf7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ti8v2rjLPtrbOQf7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ti8v2rjLPtrbOQf7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ti8v2rjLPtrbOQf7 .actorPopupMenu{position:absolute;}#mermaid-svg-ti8v2rjLPtrbOQf7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ti8v2rjLPtrbOQf7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ti8v2rjLPtrbOQf7 .actor-man circle,#mermaid-svg-ti8v2rjLPtrbOQf7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ti8v2rjLPtrbOQf7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} @Async @TransactionalEventListener(AFTER_COMMIT) 触发 保存判定对象(before)execute2Resp(chainName, ctx)各组件校验/算分(写PENDING)链成功保存 record(status=PENDING原因齐全)publishEvent(AFTER_COMMIT)真正执行 add/reward(幂等: 业务号+用户)成功回写 SEND_SUCCESS + time

这样:

  • 判分失败(业务拒绝)不执行任何下发;
  • 判分通过才发,且发的动作被隔离在事务外,失败可重试/可补;
  • 最终一致性由"状态机 + 幂等键 + 定时兜底 + 人工补发"兜住。

6. 经验与坑(都是实践里踩过的)

# 坑 / 注意 建议
1 组件名与链名强绑定@Component("xxx") ↔ flow.xml xxx 命名统一、改一处改两处;写个测试校验 Bean 存在
2 print-execution-log: true 生产日志量很大 按环境开关(uat 开 / prod 关),或日志采样
3 规则文件静态化(flow.xml 打 jar) 需要时扩展从 Nacos/DB 动态加载,附灰度开关
4 Context 膨胀(组件多了当缓存袋) 约定:组件内查询放组件、Context 只放跨组件共享结果
5 异步执行无自动重试/丢失窗口 走 MQ + 幂等键;或本地失败表 + 定时重发
6 组件抛异常会把整链/事务回滚 明确"业务拒发用 setIsEnd,系统故障才抛异常"
7 缺少单测 至少为一条主链补端到端 Junit(mock 外部依赖)

长度换来的可维护性收益

指标 改造前 改造后
加一个校验 改主流程函数 + 重测 加一个组件 + 改 flow.xml 一行
调整顺序 动大段代码 调 XML 里 THEN 顺序
按活动配置开关 改代码/发版 读配置(rewardNodes)
定位拒发原因 打日志翻代码 record.reason 直接给前台/后台

7. 结尾决策清单(什么时候用/不用 LiteFlow)

该用:

  • 一段固定流程 + 若干可插拔校验/计算节点;
  • 节点顺序/是否启用希望可配置
  • 需要"分步可观测、单节点可测";
  • 已在 Spring 生态里,不想引入第二套规则语言。

别用:

  • 规则数量巨大、规则之间有推理/冲突消解需求 → 这是 Drools 的地盘;
  • 只是一次性 2~3 个 if → 不要造复杂;
  • 对规则热更新有强诉求且有编排之外能力(如规则文件管理 UI)→ 需要在用过 LiteFlow 后再评估(它支持扩展到配置源,但完整管理端要自己搭)。

附:文中的图清单

类型 位置
架构概念图(Chain/Node/Context/Slot) mermaid flowchart §3
状态机图(短路 vs 回滚) mermaid stateDiagram §4.3
判分/执行分离时序图 mermaid sequenceDiagram §5.2

本文为脱敏实战篇:机制(flow.xml/EL/setIsEnd/Context/execute2Resp)为真实所写,业务与代码均以示意呈现。欢迎结合自身项目替换业务字段后使用。

相关推荐
学渣超1 小时前
记一次复杂业务功能重构——从过程式长链路,到模板方法构建体系
java·架构·代码规范
Bs_MoneyMagnet2 小时前
基于springboot+vue的旅游景区点评系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·旅游
夜不会漫长2 小时前
C++:类和对象(2)
java·javascript·c++
IT毕设实战小研2 小时前
基于大数据的国内主要农作物产量趋势分析与可视化
android·java·大数据·django·课程设计
星辞秋2 小时前
Java 入门实战:从零搭建环境到写出第一个可用的命令行工具
java
ch.ju2 小时前
Java—idea知识:如何创建新项目
java·开发语言·intellij-idea
土司大王2 小时前
LeetCode hot100——74.搜索二维矩阵:Java 二分模板
java·算法·leetcode
2302_1112 小时前
java依赖小结
java·运维·数据库
sunshine22 girl3 小时前
Java学习三 高级语法3, 方法
java·学习