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/规则配置/计算结果)
- Chain :
flow.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)为真实所写,业务与代码均以示意呈现。欢迎结合自身项目替换业务字段后使用。