资讯动态

低代码平台动态引擎设计与实践:Liquor规则配置化与热更新

发布时间:2026/9/10 0:40:59 来源:尧图企业网站定制
做低代码平台这几年我最大的体会是平台好不好用不看你拖拽组件做了多少而看业务逻辑能不能“动”起来。我说的“动”不是改个字段、换张表单而是不重新发版、不重启服务就能动态修改一段计算规则、一个审批分支、一条外部接口对接逻辑。这套东西在 Java 技术栈里尤其难做因为 Java 天生偏静态、强类型跑起来以后想再改行为绕不开类加载、表达式解析、安全沙箱这一堆问题。我手上这套内部项目的代号叫 Liquor它就是我们低代码平台里的那层“动态引擎”。Liquor 解决的核心问题很简单把平台里最容易变的规则、脚本、数据映射、接口编排全部配置化、可热更新让开发者和实施顾问不用碰代码就能调业务。它适合谁适合那些正在自研低代码平台、规则引擎、或者想把现有 Java 项目里硬编码逻辑抽出去做成可配置能力的团队。这篇文章我会从为什么需要动态引擎、Liquor 的整体设计、如何落地一个动态规则场景以及踩坑排查这四块展开把我实际运行这个引擎的经验全部倒出来。1. 为什么低代码平台需要一个“动态引擎”1.1 低代码平台的核心矛盾低代码平台的卖点本来是“快”但真正到业务现场就会发现快只是第一层。第二层是“改起来也得快”。业务方今天说要调运费规则明天说过滤逻辑要加一个条件如果你每次都得改 Java 代码、走发布流程那这个低代码平台本质上就是个“低效率代码平台”。做久了你会发现平台里的大部分变更都集中在少量逻辑点上费用计算、状态流转、数据校验、消息内容拼接、外部接口路由。这些逻辑的特点是变化频繁、逻辑不复杂、但每个租户/每个业务线都有自己的版本。如果把它们写死在 Java 里你维护的就是一大堆 if-else 和策略类如果把它们做成配置你就需要一个能安全、高效、可观测地执行动态逻辑的引擎。Liquor 就是在这个矛盾里长出来的。它不是要取代平台的主流程而是给主流程开一个口子让“该变的”能变“不该变的”保持稳定。1.2 Liquor 引擎在平台中的定位我习惯把低代码平台从底往上分四层数据层、能力层、编排层、交互层。Liquor 的位置在能力层和编排层之间它可以被理解为“逻辑执行底座”。具体点说当平台上的一个动作被触发时比如用户提交了一张订单平台会先走数据校验再走费用计算然后走审批流最后写库发通知。原来的做法是每个环节都调不同的 Java 服务现在改成了每个环节都先查一下 Liquor 配置中心看看这个租户有没有自定义规则如果有就动态执行没有就走默认实现。这样设计有个明显的好处接入方可以做到“默认逻辑开箱即用特殊逻辑按需配置”。而且 Liquor 自己不感知业务它只管拿到一段规则配置、一批输入参数然后返回执行结果。业务语义全在配置里引擎不碰。1.3 选型前必须先想清楚的三个问题在做 Liquor 之前我们内部其实争论过好几轮。最核心的三个问题如果你也想自研动态引擎我建议先想明白第一动态到什么程度只支持简单表达式判断还是需要完整的脚本流程这决定了你要引入的是 SpEL、Groovy、还是干脆嵌一个 JVM 语言运行时。提这个是因为很多团队一开始只想做规则判断后来需求一变又得支持循环、中间变量、调用外部服务架构推倒重来非常痛。第二谁来写规则如果是研发写那可以用 Groovy、Java 语法如果是产品经理或实施顾问写那必须提供可视化规则编辑器底层的脚本由引擎生成。Liquor 的定位是两种都支持既有面向研发的脚本模式也有面向业务的规则集模式。第三安全边界怎么收动态执行代码必然涉及类加载和反射配置错了可能影响主流程甚至被注入恶意逻辑。这个问题不能等上线后再说必须在设计阶段就定好沙箱机制、资源隔离、权限控制。这三个问题想清楚你才不会把动态引擎做成一个“能跑就行”的工具而是做成一个“跑得稳、出问题能查、性能扛得住”的底座。2. Liquor 的整体设计与核心能力拆解2.1 引擎的分层架构Liquor 的整体结构我按“元模型、执行、治理”三个维度来组织。元模型层负责定义规则和脚本的结构比如一个规则由条件、动作、优先级、版本、生效时间组成执行层负责把元模型翻译成可执行逻辑比如把 JSON 规则编译成 SpEL 表达式链或者把 Groovy 脚本编译成 Class 对象缓存起来治理层负责配置管理、版本灰度、日志追踪、熔断降级。这个分层带来的直接好处是三块可以独立演进。元模型层是纯数据定义不依赖 Spring执行层只依赖配置接口不关心配置存在数据库还是文件里治理层是 Web 管理端通过 API 和引擎交互。这样拆开以后引擎核心可以单独打成一个 jar 包任何 Java 项目都能嵌入使用不用把整个低代码平台都引进来。在实际运行中Liquor 每次执行请求会走这样一条链路接收执行请求 → 从缓存取元模型 → 校验输入参数 → 执行条件匹配 → 执行动作逻辑 → 记录执行日志 → 返回结果。如果某一步发生异常引擎会捕获并落日志同时支持降级到默认行为避免一个动态规则配置错误拖垮整个平台。2.2 表达式与脚本的执行模型Liquor 的执行模型是混合的。简单判断用 SpELSpring Expression Language复杂逻辑用 Groovy数据映射用 JSONPath。为什么这么选我一个个说。SpEL 的好处是 Spring 生态自带不需要额外引入脚本引擎安全性相对好控制适合做“条件表达式”。比如amount 100 region CN这种解析快也不怕用户写出无限循环。Groovy 则适合做完整的一段逻辑比如多步骤计算、循环处理列表、调用平台内部服务。Groovy 脚本可以编译成 Class性能比逐行解释高很多但代价是要自己做类缓存和沙箱。在 Liquor 里我定义了一套配置协议。规则的执行单位叫Rule由多个Condition和多个Action组成。条件部分支持 SpEL动作部分可以是 Groovy 脚本、HTTP 调用、或者数据映射表达式。执行时引擎先跑所有条件按优先级排序后执行命中的动作动作结果会合并到上下文里供后续规则使用。这种模型最大的价值在于规则可以拆得很细每个规则只负责一小块逻辑组合起来却可以完成一个完整流程。比如运费计算可以拆成“基础运费规则”“重量加价规则”“区域优惠规则”三个规则独立配置、独立测试、按顺序执行。业务方想调价只需要改其中一条规则不影响其他逻辑。2.3 安全沙箱与资源隔离动态执行代码最怕的就是“脚本里写死循环把 CPU 打满”或者“脚本里反射调用系统命令”。在这方面 Liquor 吃过亏所以现在的沙箱设计是认真做的。第一层是访问控制。Groovy 脚本执行前会经过一个GroovyClassLoader的白名单校验禁止解析System、Runtime、ClassLoader、Thread等关键类。同时通过SecureASTCustomizer限制语法比如禁止 import 任意类、限制方法调用范围只允许调用 Liquor 主动暴露的 API 和上下文对象。第二层是资源限制。每次脚本执行都必须带着超时时间默认 3 秒超时就中断线程并记录告警。执行线程池有独立的队列容量和拒绝策略脚本执行并发太高时快速失败而不是无限排队。这个设计在高峰期非常关键不然一个脚本卡住整个平台的请求线程都会被拖住。第三层是数据隔离。Liquor 的上下文对象是一个Map脚本只能访问到引擎主动放入的数据拿不到应用的其他类。所有写操作都被拦截脚本里不能直接改外部变量只能通过引擎提供的out对象返回结果。这样一来即使脚本有 bug影响范围也被控制在一次执行内部不会污染公共状态。2.4 规则编排与流程编排光有单个规则还不够真实业务往往是多个规则串起来。Liquor 的编排能力分两层规则集RuleSet和流程Flow。规则集解决“同一步逻辑的多种策略”问题。比如不同的客户等级适用不同的折扣计算规则集按优先级从上往下匹配命中即停。这个类似 Drools 的 agenda但比 Drools 轻很多因为我们是纯表达式匹配不引入完整规则引擎。流程解决“多步逻辑的顺序串联”问题。流程可以配置节点列表每个节点引用一个规则集、一个脚本、或一个外部服务调用。节点之间通过上下文传参支持分支条件、并行执行、失败重试。流程引擎本身很薄不做复杂的 BPMN 建模只做简单的 DAG 执行因为低代码平台里太重的工作流应该交给专门的流程引擎Liquor 负责的是“逻辑自动化的最后一公里”。实际配置一个流程时我会把它设计成 JSON 结构。一个流程由节点数组组成每个节点有type、ruleId、next、errorHandler等字段。这种表达方式有个好处和前端可视化编辑器天然契合前端画布上的一个方块就是一个节点连线就是 next 指针整个流程的定义可以直接落库也可以导出导入做环境迁移。3. 实操落地从零接入一个动态规则场景3.1 环境准备与依赖引入Liquor 的核心是一个 Spring Boot Starter接入方式很简单。我在项目里只需要引入两个依赖一个是核心引擎一个是管理端 API。核心引擎负责执行管理端 API 负责读写规则配置。版本上我建议和 Spring Boot 的主版本对齐避免类冲突。dependency groupIdcom.yourorg/groupId artifactIdliquor-core/artifactId version1.4.0/version /dependency dependency groupIdcom.yourorg/groupId artifactIdliquor-admin-api/artifactId version1.4.0/version /dependency引入依赖后需要在 application.yml 里打开引擎开关和配置存储方式。Liquor 支持多种配置存储本地开发用文件测试环境用数据库生产环境建议走配置中心。我通常的做法是本地用liquor.storelocal测试和线上用liquor.storedb并接一个 Redis 做缓存失效通知。liquor: enabled: true store: db cache: type: redis ttl: 300 executor: core-pool-size: 8 max-pool-size: 32 queue-capacity: 500 sandbox: timeout-ms: 3000 max-script-size-kb: 32这些参数看着简单但每个都值得单独说。core-pool-size和max-pool-size决定并发执行能力不是越大越好因为脚本执行吃 CPU线程太多反而增加上下文切换。我压测下来的经验是单机 8 核的容器核心线程 8、最大 16 就已经很稳了。timeout-ms是安全底线设太短会把正常慢脚本误杀设太长又起不到保护作用3 秒是平衡值。max-script-size-kb限制脚本体积防止有人塞入超大脚本拖垮编译性能。3.2 用 Liquor 实现一个动态运费计算规则我拿一个实际场景来演示电商订单的运费计算。需求是订单金额满 99 免运费不满 99 按重量计价首重 1kg 8 元续重每 500g 加 2 元如果用户是会员在结果上再打 8 折。这个需求如果用 Java 写死就是一段 if-else。但业务方跟我说过两天可能满减门槛要调成 199会员还要分等级区域不同续重价格也不同。所以我把这段逻辑完全交给 Liquor。先在管理端配置规则集。第一条规则的名称是“满额免运费”条件是order.amount 99动作是设置shippingFee 0并终止后续规则。第二条规则名称是“会员折扣”条件是user.isMember true动作是设置baseFee baseFee * 0.8。第三条规则是“重量计价”不设条件优先级最低计算逻辑走 Groovy 脚本def baseWeight 1.0 def baseFee 8.0 def extraWeight order.weight - baseWeight def extraUnits Math.ceil(extraWeight / 0.5) def shippingFee baseFee extraUnits * 2.0 out.set(shippingFee, shippingFee)这里有个细节三条规则的执行顺序不能靠录入顺序而是靠priority字段。我在第一条规则上设置了priority 10和terminate true第二条priority 20第三条priority 30。如果只设置顺序不设置终止标识命中满额免运费后还会向下继续执行把运费又从 0 算回去了这是大多数规则引擎使用者的理解偏差。接入代码很简单。在订单结算服务里我注入RuleEngine构造上下文然后调用一次执行就能拿到运费结果。RuleContext ctx new RuleContext(); ctx.setParam(order, order); ctx.setParam(user, user); RuleResult result ruleEngine.execute(shipping_fee_rule_set, ctx); BigDecimal shippingFee (BigDecimal) result.getData().get(shippingFee);关键点在于业务代码完全不感知运费逻辑的细节。以后业务方把满额门槛从 99 改成 129我只需要改配置不需要动代码、不需要发布。这听起来简单实际价值非常大。我在几个公司都见过这种场景一个算价逻辑分布在多个服务里每次调价都要协调一堆人改代码用 Liquor 之后至少这类业务变更从“研发任务”变成了“配置任务”。3.3 动态数据源与动态接口的扩展运费计算只是最简单的例子。Liquor 里我做得比较重的能力是动态数据源和动态接口。动态数据源解决的是“不同租户的数据结构不一样但查询逻辑相似”的问题。比如平台要展示订单列表A 租户的订单有门店字段B 租户没有。原来的做法是给表加一堆冗余字段或者做成 EAV 结构查询时拼 SQL。用 Liquor 的做法是在流程里加一个“数据查询节点”配置好 SQL 模板和 JSONPath 映射执行时根据租户标识动态拼出 SQL查询结果再映射成统一结构返回。这样做要注意两个坑。第一个是 SQL 模板不能用字符串拼接要用预编译?占位符加白名单校验防止注入。第二个是动态查询结果不能直接塞回业务对象要先转成Map再由数据映射节点转换避免结构不匹配导致ClassCastException。动态接口是面向外部对接场景的。低代码平台经常要把数据推给第三方或者从第三方拉数据。每个第三方的报文格式都不一样鉴权方式也不一样。Liquor 支持配置一个“接口节点”里面填请求地址、请求头、鉴权参数模板、报文映射规则。执行时引擎根据配置动态发起 HTTP 调用再用 JSONPath 从响应里提取字段写入上下文。我见过一个很折腾的对接案例某个物流接口要求先调一个 token 接口拿凭证凭证有效期二十分钟然后调下单接口时要把 token 放在 header 里报文里还要按车牌号散列取签名。这套逻辑如果用 Java 写每个新对接方都要开发联调用 Liquor 配置两个接口节点加一个 Groovy 脚本实施人员在后台点几下就能上线。当然能这样做的前提是引擎把幂等、超时、重试、熔断这些通用能力都做好了配置的人只需要关心业务映射本身。3.4 性能调优与缓存策略动态引擎最容易被诟病的就是性能。Liquor 在这方面做了三件事脚本编译缓存、规则集缓存、表达式预解析。Groovy 脚本的编译开销很大所以绝对不能每次执行都重新编译。Liquor 内部维护了一个 ConcurrentHashMapkey 是脚本内容的哈希值value 是编译后的Class。同一个版本、同一段脚本只要内容不变第二次执行直接走类实例化不再编译。配合版本发布接口脚本升级时自动清掉旧缓存新版本流量再自然切换。规则集缓存也不是简单的全量缓存。我的策略是双层第一层是本地 Caffeine 缓存TTL 设置 5 分钟第二层是 Redis 缓存TTL 设置 1 小时。配置变更时管理端主动发一个版本号递增事件各节点收到事件后清除本地缓存重新加载最新版本。这套机制能保证最多 5 分钟的最终一致性对绝大多数业务场景足够。如果你要做秒级生效可以把 Caffeine TTL 调短或者直接用 Redis Pub/Sub 通知。压测方面我做过一组对比。同样一段运费计算逻辑纯 Java 实现单线程执行一万次大约 120msGroovy 脚本编译缓存后执行一万次大约 350msSpEL 表达式执行一万次大约 220ms。动态执行的性能是静态代码的 2 到 3 倍但对单个请求来说几百微秒的差距用户根本感知不到。真正的性能瓶颈往往不在引擎而在脚本里有没有写数据库查询、有没有调外部接口、有没有在循环里做耗时的 IO 操作。所以我在引擎里加了一个链路追踪能力脚本执行耗时超过 100ms 就记录慢日志超过 1 秒直接报警定位到具体规则和节点。4. 常见问题与排查技巧实录4.1 问题和排查速查表Liquor 上线这段时间我总结了一张问题排查速查表覆盖了我遇到的大部分线上问题。现象可能原因排查手段规则不生效本地缓存未过期或版本号未递增检查版本号、刷新缓存、看执行日志脚本执行超时脚本里有死循环或耗时 IO看慢日志、终止执行、优化脚本逻辑表达式解析异常SpEL 语法错误或上下文缺字段使用管理端自带的表达式测试工具规则命中顺序不对优先级设置错误或终止标识缺失检查 priority 和 terminate 配置发版后被旧规则覆盖多环境配置未同步用规则导入导出工具做环境迁移脚本里拿不到用户对象上下文字段名不一致检查上下文 setParam 的 key并发高峰执行排队线程池配置过小调整 core-pool-size 和 queue-capacity脚本修改后不生效编译缓存未失效检查脚本内容哈希是否变化这张表不复杂但排查价值很高。很多时候线上出问题不是引擎 bug而是缓存、版本、字段名这些“低级但隐蔽”的原因。4.2 三个典型坑的复盘第一个坑是 Groovy 沙箱逃逸。早期版本的白名单校验不够严有用户在脚本里用反射拿到了Runtime并执行了系统命令。虽然只是内部测试但吓得我连夜把所有脚本全部禁用重新设计沙箱。现在的规则是Groovy 脚本不允许import任何类所有需要的能力通过上下文对象公开方法暴露编译时做 AST 校验运行时做调用拦截。不要迷信单一校验机制编译期和运行期要双重把关。第二个坑是规则命中后的“僵尸逻辑”。有一次业务方反馈某个租户的订单总是算错运费查了半天发现是旧版本规则没有下线新规则和旧规则同时在执行列表里由于旧优先级高一直命中旧逻辑。后来我加了一条铁律任何规则变更必须走版本发布旧版本自动标记为失效不能只编辑不发布。管理端也要有“生效规则预览”功能规则集上线前可以查看当前环境的完整规则快照。第三个坑是执行日志的 IO 压力。引擎每执行一次规则就写一条日志高峰时每秒上万条直接把数据库打爆了。现在的方案是日志先写内存环形缓冲异步批量落库执行结果和耗时指标走 Prometheus 监控只有异常和慢日志才会写详细链路。监控和执行日志要分离不能把日志系统当业务系统用。4.3 调试动态逻辑的实用技巧Liquor 的管理端我专门做了一个“调试台”功能。在里面可以选一个规则集填测试参数 JSON然后单步执行查看每一步的上下文变化。这个功能看着简单却是排查问题效率最高的工具。没有它你只能靠加日志和猜有它你可以像 debug Java 一样 debug 动态逻辑。除了调试台我还分享几个小技巧。第一规则集里每个节点都应该设置description写在业务上是什么意思、开发者是谁、什么时候改的。动态配置的维护成本不在写而在半年之后没人看得懂当初为什么这么配。第二表达式测试要写边界用例空字符串、负数、超大金额、缺失字段都要在调试台里跑一遍。第三执行结果建议打印一份“规则命中摘要”包含命中了哪些规则、每个规则耗时多少、输出了什么值。这段摘要日志平时不落库只在用户反馈异常时手动开启能省去大量抓包排查的时间。还有一个容易被忽视的点上下文里的字段命名要有约定统一用snake_case还是camelCase必须在团队里定死。我见过一个项目有的脚本用orderNo有的用order_no排查时人直接崩溃。Liquor 的配置协议里我把标准字段名做了映射层命名不规范时管理端会给出校验警告但根本上还是靠团队规范约束。最后说一个我在实际运行 Liquor 后才明白的道理动态引擎的代码复杂度并不高真正的复杂度在于它让“谁都可以改逻辑”之后怎么保证改了不闯祸。我的经验是两条腿走路技术上做好沙箱、超时、缓存、监控管理上做好版本、预览、审批、审计。两条腿都齐了Liquor 才能从一个“会跑的引擎”变成一个“跑得让人放心的引擎”。如果你也在折腾低代码平台的动态化我建议别一上来就搞特别重的规则引擎或者自研脚本语言先用表达式加轻量脚本把 80% 的配置化需求覆盖掉跑通了再逐步加编排、加治理。Liquor 这套玩意儿的核心设计思路就是能不做的事坚决不做但做出来就一定要稳、要可查、要可控。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价