
业务逻辑越写越像意大利面,改个活动规则得走一遍 CI/CD,这毛病在不少团队里都见过。运营今天加个库存拦截,明天改个阶梯满减,技术侧就得跟着改代码、补单测、等发布。等到线上跑起来,往往已经错过了最佳窗口期。
这篇文章不聊虚的架构概念,直接复盘我们在线上系统里用 LiteFlow 替换硬编码路由的实际过程。包括怎么跟 Spring Boot 无缝对接、怎么结合 Nacos 做秒级热更新,以及线上踩过的坑和调优细节。
一、 痛点:硬编码的维护成本与热更新之困
早期做营销活动或审批流,大家习惯用策略模式 + 工厂类把业务拆出来。一开始确实清爽,但随着运营策略越来越细,前置条件堆到七八个,路由逻辑就开始失控了。if (A && !B || (C && D)) 这种嵌套一多,新人根本不敢动,动错了就是线上客诉。
更麻烦的是发布流程。规则逻辑写死在 Jar 包里,每次变更必须走"开发 -> 编译 -> 打包 -> 流水线 -> 重启"全套。遇到风控策略需要紧急拦截,或者大促期间临时切量,这种滞后性直接导致业务机会流失。业务方抱怨技术响应慢,技术团队抱怨业务变来变去,最后互相消耗。
我们当时的解法思路很明确:把"控制流"和"业务数据"剥离开。规则不再是代码,而是配置。技术只负责提供原子组件和调度框架,业务逻辑交给声明式的编排文件。
二、 为什么选 LiteFlow 而不是 Drools
Java 规则引擎圈子里,Drools 确实是老牌强者。Rete 算法处理海量事实匹配、冲突消解、历史规则回溯,能力没得说。但实际落地时,我们发现几个很现实的问题:
DRL 语法学习曲线太陡,调试起来基本靠猜。跟 Spring 集成得自己写适配层,Session 生命周期管理稍不注意就引发内存泄漏。对于只是需要动态拼装流程、做条件路由、组合策略的场景,Drools 显得太重了,属于杀鸡用牛刀。
LiteFlow 走的是另一条路。它不玩事实推理,专注流程编排。底层用 EL 表达式描述节点拓扑,直接映射成 DAG 有向无环图执行。优势在于三点:
- 零侵入 :原生提供
liteflow-spring-boot-starter,Bean 直接当节点,不用写额外的适配代码。 - 上手快:EL 语法跟 SpEL 很像,熟悉 Spring 的工程师看一遍文档就能写链,半天就能跑通第一个 Demo。
- 热更新干净:内置双缓冲切换机制,刷新规则时在后台编译新图,切引用是原子操作,正在执行的请求不受影响。
除非你的场景是万级风控规则库、需要复杂的事实推导和冲突裁决,否则对于 80% 的动态路由、策略组合、微服务 API 聚合需求,LiteFlow 更贴近 Java 开发习惯,运行时开销也更可控。
三、 核心机制:别把 EL 当脚本,它是路由 DSL
用 LiteFlow 之前,得先扭转一个观念:它不是脚本执行器,而是一套面向流程的声明式编程模型。理解它,抓住四个点就够了。
1. 组件(Component)就是普通的 Spring Bean
每个原子逻辑抽成一个类,继承 NodeComponent。严格遵守单一职责,一个组件只管一件事,比如校验用户资格、计算折扣、查库存、记日志。
java
@LiteflowComponent("couponValidator")
public class CouponValidatorComponent extends NodeComponent {
@Override
public void process() {
MarketingContext ctx = this.getContextBean(MarketingContext.class);
if (ctx.getCoupon() != null && ctx.getCoupon().isExpired()) {
throw new LiteFlowBizException("优惠券已过期");
}
ctx.setDiscountAmount(ctx.getCoupon().getAmount());
}
}
组件内部严禁持有任何可变状态 。中间数据全部走 Context 传递,这样组件天然无状态,水平扩展时不用考虑线程安全问题。
2. EL 表达式描述拓扑
EL 不是图灵完备的编程语言,它是专门用来描述节点怎么流转的 DSL。语法很直观:
THEN(a, b, c):串行执行,默认失败阻断。WHEN(a, b):并行执行,适合把耗时 I/O 操作扔出去异步跑。SWITCH(x).to(a, b):组件x返回字符串,引擎按返回值跳转。CATCH(TRY(a), ON_EXCEPTION(b)):节点降级/补偿路由。
这些表达式可以任意嵌套。比如:THEN(preCheck, WHEN(calcA, calcB), SWITCH(route).to(success, fail))。
3. Context 是数据总线,不是垃圾袋
上下文贯穿整条链,官方推荐用强类型 POJO。执行时通过 this.getContextBean(OrderContext.class) 拿。
实际开发里最容易犯的错误是把 Context 当全局变量用,往里塞一堆无关对象,甚至传大集合。这会导致内存暴涨,GC 频繁。上下文设计原则就两条:按需传递,用完不用的字段及时置空。LiteFlow 内部用 ThreadLocal 管理上下文生命周期,请求结束自动清理,但大对象自己得兜底。
4. 编译期静态分析 + 运行期动态调度
引擎启动或热更新时,会把 EL 字符串解析成 DAG 图。依赖关系、并发控制、线程池分配都在编译期确定好。运行时只负责按图调度。这种设计兼顾了灵活性和执行效率,避免了每次请求都去动态解析表达式的性能损耗。
四、 Spring Boot 集成与 Nacos 热更新实战
集成过程很平滑,核心工作量其实在于把规则源从本地文件切到配置中心,并配好刷新逻辑。
1. 基础依赖与配置
xml
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>2.12.2</version>
</dependency>
application.yml 里把开关打开:
yaml
liteflow:
rule-source: classpath:flow/
print-execution-log: true # 本地开发开着,生产关掉走日志中心
retry-count: 0 # 不推荐引擎层重试,业务降级自己控
when-max-wait-second: 3 # WHEN 并行节点超时时间
2. 规则文件长什么样
用 XML 最直观,结构清晰也容易做 diff。
xml
<flow>
<chain name="vip_discount_flow">
THEN(userQualification,
WHEN(baseDiscountCalc, vipTierCalc),
SWITCH(stockCheck).to(applyFinalDiscount, rejectOrder),
auditLogRecord);
</chain>
</flow>
Spring Boot 启动时,LiteflowSpringAutoConfiguration 自动扫描注册 Bean,解析 EL,把执行图缓存到 FlowExecutor 里。
3. 结合 Nacos 做秒级热更新
生产环境绝不能把规则写死在本地。我们用 Spring Cloud Alibaba 的 @NacosConfigListener 监听配置变更,拿到新规则后直接调引擎的刷新 API。
java
@Slf4j
@Component
public class LiteFlowRuleReloader {
@Autowired
private FlowExecutor flowExecutor;
@NacosConfigListener(dataId = "liteflow-marketing-rules.yml", group = "BUSINESS")
public void onRuleChange(String newRules) {
if (StringUtils.isBlank(newRules)) return;
try {
// 引擎内部会双缓冲构建新图,构建完原子切换引用
flowExecutor.reloadRule(newRules);
log.info("规则热更新完成,当前链数: {}", flowExecutor.getChainMap().size());
} catch (Exception e) {
log.error("规则热刷新失败,旧链路不受影响,请及时介入", e);
// 这里触发钉钉/企微告警即可
}
}
}
线上经验:
- 刷新前一定要做语法校验。LiteFlow 提供了
LiteflowConfigValidator,可以先在测试环境或内存里跑一遍,防止格式错误导致刷新中断。 - 配置中心必须带版本管理和回滚能力。规则推错是常事,能一键切回上一个稳定版本比什么都强。
- 涉及资金、支付的核心链,建议加一层"人工审批 + 定时生效"机制,别把直接推送的权限全开给运营。
五、 实际场景怎么用
营销活动策略拼装
大促期间,运营临时要上"满300减50,叠加新用户专享券,限特定类目,库存<10降级"。传统做法改代码发版至少两天。用 LiteFlow,技术侧提前拆好原子组件:checkUserStatus、calcThreshold、checkCategory、checkStock、applyCoupon。运营侧通过低代码后台拖拽或改配置,EL 拼好直接推。技术零改代码,秒级生效。
动态审批路由
审批流不再是简单的线性流转,得按"角色+金额+部门+时间"动态分叉。用 SWITCH 组件查策略表,返回值决定下一节点。组织架构调整时,只需要改策略表数据,审批流自动跟着变,不用重新发版。
SaaS 多租户计费策略
不同租户套餐对应不同计费模型。把"计价逻辑"封装成独立组件,EL 当路由中枢。新上一个"大客户折扣"策略,加个折扣组件,在链里插一行 WHEN(largeClientDiscount, standardDiscount) 就完事了。A/B 测试、灰度切量都很方便。
六、 线上跑过才知道的避坑指南
引入规则引擎只是开始,怎么在生产环境把它跑稳,才是考验工程能力的地方。
1. 组件粒度别放太宽
见过有人把几十个业务判断全塞进一个组件,叫 AllInOneComponent,这完全违背了 LiteFlow 的设计初衷。组件必须对应一个明确的动作或一次外部调用。依赖要克制,尽量别在组件里乱 @Autowired 其他业务 Bean。如果调第三方接口,自己配好熔断和超时,别指望引擎层替你兜底。
2. 异常隔离是底线
链上某个节点抛异常,绝不能把整条链拖死。LiteFlow 的 CATCH 和 ON_EXCEPTION 很实用:
xml
<chain name="payment_chain">
THEN(
CATCH(
TRY(payComponent),
ON_EXCEPTION(fallbackAuditComponent)
),
notifyComponent
);
</chain>
生产规范就几条:业务异常抛 LiteFlowBizException,系统异常走统一拦截;核心 I/O 必须配超时;降级节点必须幂等,保证数据最终一致。
3. 线程池与性能调优
纯内存路由场景下,单实例跑十几个轻量组件,QPS 过万很正常。但一旦涉及 WHEN 并行,线程池配置不对直接雪崩。when-max-wait-thread 别瞎填,I/O 密集型场景适当放大到 200~400,配合有界队列。别用默认队列,队列爆了直接丢请求或者触发降级,总比 OOM 强。
4. 可观测性必须提前做
开发环境开着 print-execution-log 看执行轨迹没问题,生产环境全量打印日志会把磁盘打满。正确做法是:
- 接入 SkyWalking 或 Arms,引擎自带 Span 暴露,能直接看到节点瀑布图和耗时。
- 用 Micrometer 暴露
liteflow.chain.execute.total、liteflow.node.cost.time等指标到 Prometheus。Grafana 配个面板,单链 P95 超过 500ms 持续几分钟直接告警。 - 日志按
traceId关联,排查问题时直接拉整条链的上下文,别去翻散落的打印。
5. 规则版本管理
规则即资产。每次变更必须记录 EL 内容、操作人、时间、关联需求。我们内部做了个简单的规则快照服务,热更新时存一份快照 ID。出问题了一键回滚,引擎双缓冲切旧图,恢复时间基本在 2 秒内,业务无感知。
写在最后
LiteFlow 不是万能药,它解决的是"业务编排、策略拼装、动态路由"这类高频变更场景。把硬编码逻辑抽离成声明式配置,配合配置中心的热刷新能力,研发确实能从"人肉发版机"的角色里解放出来。
架构选型从来不是比谁的技术栈更炫,而是看哪个工具能最顺手地解决当下的痛点。规则引擎用得好,是提效利器;用得不好,就是多了层黑盒,排查问题更头疼。把组件拆干净、把异常兜住、把监控做全,剩下的,交给业务去迭代就行。
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/
