ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于SpringBoot的规则编排可视化系统设计与实践

基于SpringBoot的规则编排可视化系统设计与实践 这几天在公司把一套基于 SpringBoot 的规则编排可视化系统从零搭到了线上运营同事总算不用每次改活动规则都来找我排期了。趁着热乎劲儿把整个设计思路、技术选型和踩坑过程整理出来给同样被“业务逻辑变更频繁”折磨的朋友一个参考。这套东西说白了就干一件事让不懂代码的业务人员在界面上拖拖拽拽、点点选选就能配置出“满100减20且仅限新用户”或者“金额大于5000且命中风控名单则人工审核”这类复杂业务规则配置完直接生效不用发版、不用重启。文章里我会把规则模型怎么设计、表达式引擎怎么选、可视化界面怎么和 SpringBoot 后端打通以及上线后遇到的坑全部摊开来讲。1. 规则编排可视化的核心价值与应用场景1.1 业务背景规则变更需求的常态化和开发瓶颈几乎所有业务系统都会面临同一个问题业务规则比代码变得快。运营今天要做一个“新人专享五折券”的活动明天要调整成“满三件打八折且只限指定类目”后天又要在风控流程里加一条“同一IP下超过5个账号下单需要人工审核”。这些规则本身不复杂但架不住组合多、变更快。如果用传统方式每次变更都要走一遍“提需求 - 排期 - 改代码 - 测试 - 发版”的流程。哪怕改一个数字最快也得半天遇到紧急活动或者大促前的临时调整开发就成了整个业务的瓶颈。我当时接手这个需求的时候业务方已经攒了一堆规则调整的单子。最夸张的一次运营为了一个促销活动一周内提了三次变更每次都是上线当晚发现规则有漏洞又急着往回改。这种循环不仅消耗开发资源更致命的是规则在代码里都是一堆 if-else时间一长谁都不敢动那块代码因为根本不知道改了这一处会影响哪条线上的哪个流程。规则编排可视化的思路就是把这些散落在代码里的 if-else 提取出来转成结构化的配置数据让业务人员在一个可视化界面里直接维护。开发只需要做两件事一是把规则执行引擎做稳二是把可视化配置界面做好用。1.2 典型应用场景营销优惠、风控拦截、审批流转落地之前先得圈定哪些场景适合用这套东西。我梳理下来主要有三类场景是刚需。第一类是营销优惠计算。这是最典型也最容易出成果的场景。优惠规则天然就是“条件 动作”的组合满足什么条件用户等级、订单金额、商品类目、是否首单执行什么动作打折、减钱、送券、包邮。而且规则变化极频繁每个活动一套规则活动之间还可能叠加互斥。第二类是风控拦截和策略配置。风控规则的特点是条件多、更新快、对时效要求高。比如“注册时间小于7天的账号下单金额超过2000需要人工审核”“同一收货手机号当天关联订单超过3笔自动拦截”。这些规则如果靠开发写死在代码里风控人员根本没法快速响应突发的刷单行为。第三类是审批流程的节点条件。现在的审批流引擎比如Flowable、Activiti一般支持条件表达式但表达式都是写在线路里的非技术人员看着一脸懵。把审批条件和打分卡做成可视化规则业务人员就能自己配置“金额大于5万且部门为销售部则走总监审批否则走经理审批”这类逻辑。当然不是所有场景都适合可视化编排。如果规则极其固定、一年都不变一次或者逻辑极其复杂、涉及多表关联的多轮循环计算那还是老老实实写代码更合适。可视化编排最适合的是“规则变化频繁、组合有一定复杂度但可控、需要业务人员自助维护”的中间地带。1.3 方案目标千人千面、在线生效、业务自助说到底做这套系统就为了三个目标。第一个目标是在线生效。规则配置完成后不需要发版、不需要重启服务配置同步到执行引擎后下一次请求立即生效。这就把规则变更的响应时间从“半天起步”压缩到“分钟级”。我当时的方案是配置保存后写库通过 Redis 发布订阅通知各实例刷新本地缓存这样即使多实例部署也能保证规则准实时生效。第二个目标是业务自助。业务人员自己进系统、自己配规则、自己测试验证、自己发布上线。开发彻底从这些琐碎的规则调整中解放出来只处理引擎报错和新增的规则类型。第三个目标是千人千面。有了规则配置平台之后不同用户、不同场景看到的活动价格、营销权益可以完全不同。这就是“千人千面”的底层支撑不是每个用户写一套代码而是通过不同规则的组合计算出不同结果。2. 技术选型自己造轮子还是用框架2.1 常见方案对比Drools、EasyRules、Flowable、表达式引擎规则引擎这块Java 生态里其实有好几个现成的选择当时我都调研了一遍。Drools 是老牌规则引擎基于 Rete 算法规则用 DRL 语法编写能处理非常复杂的推理场景。但问题也明显DRL 语法学习成本高业务人员根本不可能直接写 DRLDrools 框架本身偏重引入后内存和类加载器方面多多少少会有些坑而且规则量一大调试和维护都要专门的知识储备。EasyRules 是个轻量级规则引擎底层还是 Java 代码定义规则支持用注解或者规则描述文件比如 JSON、YAML声明规则。它比 Drools 简单很多适合做“多条规则顺序执行命中即停或继续”的场景。但对复杂条件组合的支持不够灵活本质上还是扁平规则列表不好表达嵌套的 AND/OR 逻辑。Flowable / Activiti 这类工作流引擎是用来做流程编排的适合审批流、任务流但它是节点流转的思维不是“条件判断 动作执行”的思维。虽然也能实现复杂的条件网关但配置界面重学习成本也不低而且为了算一个优惠价去引入一个工作流引擎有点杀鸡用牛刀了。SQL 引擎和表达式引擎是另一个方向。本质上规则就是一个 boolean 表达式表达式引擎负责把字符串表达式解析成可执行逻辑。比如 Aviator、QLExpress、SpEL 这些都是成熟方案。用表达式引擎的好处是执行效率高、依赖轻坏处是表达式本身有语法业务人员直接写表达式仍然有门槛需要可视化界面帮忙生成表达式。综合比较之后我选择了“自研规则模型 表达式引擎执行”的路线规则的结构用一套自定义的 JSON 模型表达可视化界面负责把用户的拖拽操作转成 JSON执行引擎解析 JSON 并调用表达式引擎完成最终判断。2.2 为什么选择表达式引擎加自研规则模型这里详细说说选型理由方便你理解我为什么没有直接用现成规则引擎。第一个理由是控制力。Drools 这样的重型引擎封装层次太高出了问题很难定位。自研规则模型意味着数据结构完全可控规则存储、转换、校验、测试每一个环节都掌握在自己手里出了问题可以快速定位是模型问题还是表达式问题。第二个理由是学习成本和维护成本。Drools 在国内的使用率其实没有想象中那么高团队里真正玩过 DRL 的人很少。而表达式引擎比如 Aviator语法简单团队同学看着文档就能上手维护。可视化配置界面直接从 JSON 模型渲染不用去处理 DRL 的编译和加载机制。第三个理由是灵活性。自研规则模型可以非常贴合业务。比如我可以给条件节点定义“匹配方式”“值类型”“自定义函数”这些在通用规则引擎里要么不支持要么需要额外扩展。而我们是自己想怎么设计就怎么设计。当然自研也有代价。规则引擎的很多细节坑得自己踩比如表达式编译性能、函数注册、上下文变量传递、沙箱安全这些都得自己处理。但站在现在往回看这个选择是值得的。后面我会把每个坑怎么填都讲清楚。2.3 可视化方案选型AntV X6、LogicFlow、vue-flow规则模型定下来之后还有一个关键选型前端可视化用什么画。如果规则表达是树形结构其实就是一棵条件树那方案就多了普通表单就能实现用树形控件嵌套条件组也能做。但如果想要“连线式”的规则编排体验让用户像画流程图一样把条件、动作节点串起来就需要用图编辑引擎。我当时调研了三个主流方案。AntV X6 是国内蚂蚁集团开源的关系图编辑引擎生态比较成熟文档齐全内置了很多交互能力比如节点拖拽、连线、撤销重做、小地图等。如果是做复杂的连线编排X6 是很稳的选择。LogicFlow 是滴滴开源的前端流程图编辑框架主打逻辑编排场景API 设计得很清晰而且自带了类似“节点面板 - 画布 - 属性面板”的布局方案做规则编排很对味。vue-flow 是 React Flow 的 Vue 移植版交互流畅但在国内社区和中文文档上比前两者弱一些。我做了一个权衡连线式的编排虽然炫酷但对业务人员来说上手门槛反而更高拖拽连线乍一看直观但复杂规则一多连线在画布上交错根本看不清。最终我采用了“表单嵌套 树形展示”的方案左侧选择字段和操作符右侧实时生成可读的条件语句中间用缩进和图标表现层级关系。这样业务人员第二天就能上手不需要理解“节点”和“连线”的概念。这个取舍很重要。可视化不等于花哨关键是效率。如果界面看起来高大上但业务人员学不会反而成了负担。后面在界面设计部分我会具体展示这种方案的效果。3. 规则模型设计从逻辑到数据结构的转换3.1 原子节点设计条件节点、动作节点、组合节点规则模型是整个系统的地基这一步设计不好后面全盘皆输。我设计的核心思路是把任何复杂的业务规则拆成有限的原子节点再用树形结构组合起来。一个规则由两部分组成条件和动作。条件部分是一个条件树树的每个节点有两类条件节点Condition表达一个最小的判断单元。由三要素组成左操作数、操作符、右操作数。比如“订单金额 1000”左操作数是订单金额操作符是 右操作数是 1000。组合节点Group表达一组条件的组合逻辑只有两种AND所有子条件同时成立和 OR子条件任意一个成立。组合节点可以嵌套这样就天然支持了“(A AND B) OR (C AND D)”这种复杂逻辑。动作部分是一个动作列表每个动作由一个动作类型和一组参数构成。比如动作类型是“APPLY_DISCOUNT”参数里指定折扣力度动作类型是“SET_STATUS”参数里指定状态值。将来有新的动作类型只要后端注册一个处理函数就行前端配置界面也会自动多出一个可选动作。把规则转成 JSON 之后大概长这样{ ruleName: 新用户满减活动, conditionGroup: { logic: AND, children: [ { type: condition, left: user.isNewUser, operator: , right: true }, { type: group, logic: OR, children: [ { type: condition, left: order.amount, operator: , right: 100 }, { type: condition, left: cart.itemCount, operator: , right: 3 } ] } ] }, actions: [ { type: APPLY_DISCOUNT, params: { discountRate: 0.8 } } ] }这个 JSON 既可以直接存储到数据库也可以作为前后端交互的协议还能拿来渲染可视化界面和执行。3.2 规则执行引擎表达式解析与上下文传递规则模型定好了接下来就是执行引擎。执行引擎要做的事情非常简单接收规则 JSON 和业务上下文 Map解析条件树逐个计算条件节点最终返回是否命中以及应该执行哪些动作。但实现的时候有几个关键点要处理好。第一个是条件节点的计算。条件节点里的左操作数通常不是固定值而是一个变量路径比如order.amount它代表从上下文中取订单金额。执行的时候需要从业务上下文里把这个值解析出来。我用了一个变量解析器支持点号路径比如user.level会先取 context 里的 user 对象再取它的 level 属性。这个解析器支持 Map 和 Java Bean 两种类型毕竟有的业务上下文是 Map有的直接塞了个 Object。第二个是操作符的扩展。除了常见的 、!、、、、还需要支持 in、not in、contains、startWith、matches正则等操作符。每个操作符就是一个策略类接收左值和右值返回 boolean 结果。第三个是上下文传递。执行引擎不能只判断“命中/未命中”很多时候动作计算需要中间结果。我在引擎里设计了一个可变的执行上下文前面动作的计算结果可以放入上下文供后续动作或者条件使用。这就实现了简单的链式计算。执行引擎用了一个很经典的设计规则编译和规则执行分离。规则 JSON 从库里读出来后会先编译成一颗“可执行节点树”节点对象持有预处理后的操作符策略和值对象这样执行的时候不用反复做字符串解析和类型转换性能会好很多。3.3 整体架构配置中心加规则存储加执行引擎把整个系统的架构串起来看大概是这样的前端配置平台Vue 自研表单组件负责把规则 JSON 渲染成可视化表单用户保存时再把表单转回 JSON。后端管理服务SpringBoot提供规则的增删改查、版本管理、发布、测试接口。规则存储MySQL Redis缓存规则 JSON 存 MySQL发布后加载到 Redis本地实例再做内存缓存保证高频读取场景的性能。执行引擎SpringBoot业务系统内嵌业务代码里调用执行引擎传入场景编码和业务上下文引擎自动定位规则并执行。你可能注意到这里没有把执行引擎做成独立的微服务而是作为依赖包嵌入到各个业务系统。原因很简单规则执行是高频低延迟调用多一次远程调用就多一份网络开销和故障风险。嵌入式的方案性能最好部署最简单规则更新通过 Redis 广播通知刷新本地缓存即可。4. 核心代码实现一个可运行的规则执行引擎4.1 规则实体与条件节点定义Java 这边我定义了几个核心实体类。Rule 是整个规则的顶层对象包含规则编号、名称、场景编码、条件树、动作列表、状态等字段。Data public class Rule { private String ruleCode; private String ruleName; private String sceneCode; private ConditionGroup conditionGroup; private ListRuleAction actions; private Integer status; private Integer version; }ConditionGroup 对应组合节点Condition 对应条件节点。一个 Group 里既有子 Group 又有 Condition所以用 List在序列化时会比较麻烦。我选择了在子节点列表中用一个 type 字段区分类型这样既清楚又好扩展。Data public class ConditionGroup { private String logic; // AND / OR private ListConditionNode children; } Data public class ConditionNode { private String type; // condition or group private String left; private String operator; private Object right; private ConditionGroup conditionGroup; // 当 typegroup 时有值 }动作实体很简单就是一个动作编码加一个参数 Map。不同的动作类型处理时读取参数里的字段做不同逻辑。Data public class RuleAction { private String type; // 如 APPLY_DISCOUNT、SET_STATUS private MapString, Object params; }4.2 表达式引擎集成Aviator 的用法条件节点最终是要计算的。我在左边变量和右边常量之间做判断最开始自己手写操作符解析后来踩了几个类型转换的坑索性换成了 Aviator 表达式引擎来兜底。Aviator 是一个高性能的轻量级 Java 表达式求值引擎语法和数学表达式、布尔表达式很接近。比如order.amount 100 user.level 2这种表达式Aviator 可以直接求值。我的做法是把整个条件树在编译期转成一个 Aviator 表达式字符串条件节点用和||连接执行的时候直接调用 Aviator 求值。public class AviatorEvaluator { public static boolean evaluate(String expression, MapString, Object env) { Object result AviatorEvaluatorInstance.execute(expression, env); return Boolean.TRUE.equals(result); } }比如前面的 JSON 规则编译后的表达式是(user.isNewUser true) ((order.amount 100) || (cart.itemCount 3))Aviator 对变量名和点号路径的原生支持已经很到位user.isNewUser这种写法可以直接从 env 里嵌套取值。不过有一个坑要记住Aviator 默认把变量名大小写敏感且当变量在 env 中不存在时会抛异常。我在编译期会把规则里引用到的字段做一次白名单校验避免运行业务上下文里缺字段导致整个规则崩溃。4.3 完整执行流程从 JSON 配置到规则结果执行引擎对外暴露的接口很简单核心就是一个 execute 方法。public RuleResult execute(String sceneCode, MapString, Object context) { Rule rule ruleLoader.getActiveRule(sceneCode); if (rule null) { return RuleResult.notHit(rule); } boolean matched ConditionEvaluator.evaluate(rule.getConditionGroup(), context); if (!matched) { return RuleResult.notHit(rule); } ListActionResult actionResults new ArrayList(); for (RuleAction action : rule.getActions()) { ActionResult result actionExecutor.execute(action.getType(), action.getParams(), context); actionResults.add(result); } return RuleResult.hit(rule, actionResults); }这个流程看起来简单但真正要跑起来还要解决几个问题。第一是规则加载。ruleLoader 先从本地缓存拿规则拿不到就查 RedisRedis 也没有就查数据库。数据库加载完写回 Redis 和本地缓存。每次规则发布时清空本地缓存并推 Redis 消息各实例收到消息后自动重载。第二是类型转换。条件节点里的 right 值在 JSON 里是字符串但比较的时候可能是数字、布尔、日期。我在编译期根据 left 字段的类型元数据自动做类型转换。比如 left 是order.amount元数据里标记了是 BigDecimal 类型那 right 值就转成 BigDecimal 再比较。第三是安全兜底。条件树编译成 Aviator 表达式后如果某个字段值非法比如 null 参与比较Aviator 的行为有时候会让人懵。我统一封装了空值处理策略凡是条件节点里左值为 null一律视为条件不成立只有配置了“允许空值”的场景才特殊处理。这样就避免了规则因为个别用户数据缺失而大面积失效的问题。4.4 可视化配置界面表单加树形展示前端部分我没有用流程图的方案而是做了一个“表单驱动”的配置交互。整个配置界面分成三块。左侧是字段面板列出了所有业务场景下可用的字段。中部是条件配置区展示了当前条件树的所有节点。右侧是属性编辑区选中条件节点后在这里配置操作符、比较值、参数等。条件树在界面上的表现是缩进列表每个组合节点有一条竖线子节点带连接线图标。AND 组合和 OR 组合之间的切换直接点击逻辑按钮就行。新增条件时用户从左侧拖拽字段到目标条件组下或者点击组节点上的“新增条件”按钮在弹窗里选择字段、操作符和值。每一步操作界面都会实时生成一条自然语言的描述比如“(订单金额 100) 且 (用户新用户标记 是)”。这样业务人员不用理解数据结构看描述就知道自己配置的逻辑对不对。配置完成界面把树形结构转成规则 JSON点击“测试”可以先填一组测试数据验证规则命中结果没问题再点“发布”。整个流程没有接触任何代码也没有任何语法学习成本。5. 落地过程中的常见问题与避坑指南5.1 表达式安全问题沙箱、函数白名单自研规则引擎最容易被忽略的就是安全。规则里可以写 Aviator 表达式如果表达式引擎允许调用任意 Java 方法那有配置权限的人理论上就能通过表达式执行任意代码这等于给系统留了一个后门。我当时做安全加固主要是三层。第一层是操作符白名单。可视化界面生成表达式时只允许使用固定的操作符集合如 、!、、、in、contains 等凡是白名单之外的操作符一律不允许。这样 Aviator 表达式字符串就只能在有限的语法空间内变化。第二层是函数白名单。如果规则里需要自定义函数比如根据地址文本解析省市区我会在引擎里先注册好函数表达式里只允许调用已注册的函数。Aviator 提供了addFunction方法没注册的函数默认不允许调用。第三层是语法校验和编译缓存。规则保存和发布前都会经过一次表达式编译校验编译不通过的规则不允许发布。编译通过后的表达式会缓存起来避免每次执行都重新解析。如果你们团队用的是 QLExpress 或者 SpEL做法类似先禁掉默认的反射调用能力再开白名单。这一点安全必须做在配置阶段而不是执行阶段因为执行阶段拦截已经晚了。5.2 性能问题规则数量膨胀时的优化规则可视化之后业务人员配置规则的积极性很高规则数量很快就会膨胀。从几十条到几百条再到几千条执行引擎的性能压力就上来了。一开始我的执行逻辑很简单拿到场景编码遍历这个场景下所有规则逐条执行第一条命中的生效。规则少的时候没问题规则多了以后一次请求可能要执行几十上百个条件判断CPU 消耗明显飙升。优化分了三步。第一步是规则索引。把所有规则按场景编码存档场景编码做索引同一个场景下的规则按优先级排序执行时只取该场景下的规则子集绝不去遍历全量规则。第二步是条件预编译。规则加载的时候条件树直接编译成可执行的对象树不再运行时做 JSON 解析和字符串拼接。Aviator 的表达式字符串也缓存好避免重复编译。第三步是规则缓存加上限。本地缓存做 LRU 淘汰控制每个场景缓存的规则数量不超过阈值。支持一个场景下配置多条规则按优先级返回命中结果。性能优化之后单次规则执行的耗时从上万条规则场景下的毫秒级降到了几百微秒级别加上本地缓存命中整体几乎无感。5.3 版本管理与灰度发布规则配置是业务人员自己发布的如果没有版本管理出问题想回滚就没地方退了。我在规则表里设计了版本号字段每次发布不直接覆盖当前版本而是新增一个版本记录只有“已发布”状态的版本会生效历史版本全部留档。这样一来如果业务人员发现新规则有误可以一键回滚到上一个发布版本。后端实现也很简单回滚就是把指定历史版本的状态改为新发布原发布版本状态改为历史。灰度也做了。规则表里增加了一个灰度配置字段可以配置该规则对哪些用户生效比如按用户ID取模、按城市白名单、按用户类型名单等。执行引擎判断命中之前先做灰度过滤只有灰度范围内的请求才允许命中新规则。灰度配置看起来很基础但非常重要。大促期间改优惠规则直接全量上线万一算错了价损失不可预估。灰度发布让业务人员可以小范围验证跑了半小时确认没问题再全量放开。5.4 权限控制谁能编辑谁只能看还有一个经常被忽视的问题是权限。规则配置直接影响线上业务逻辑必须严格区分谁能编辑、谁能发布、谁能只看。我是这样安排的业务人员默认只有“编辑草稿”权限可以保存草稿但没法发布上线。发布权限单独给到团队里的一个资深运营或者业务负责人。管理员可以有全部权限包括新增字段、修改字段元数据、注册新动作类型等。前端界面根据当前用户的角色动态展示按钮后端接口也统一做了权限校验不能只靠前端隐藏按钮。权限控制这一层如果做漏了业务人员误操作发布了一条错误规则造成的影响是直接的线上损失这个责任谁都担不起。5.5 规则测试机制预置样本与运行预览规则发布前的测试环节是保证规则正确性的最后一道防线。可视化配置界面上我加了一个“测试运行”功能用户可以填一组模拟的业务数据点击执行后界面直接展示这条规则是否命中、命中的动作结果是什么。如果规则里有 and/or 组合界面还会把每个条件节点的命中情况逐条列出来方便用户判断是哪一层条件没有满足。测试的数据怎么来我提供了两个来源。一个是用户手工输入在界面上点选字段填值另一个是从线上请求抓样本数据。每个场景下都配置了请求日志抽样把最近命中的请求上下文脱敏后存起来测试时直接选一条样本数据回放看规则执行结果。这个机制上线后效果非常好业务人员自己配置完规则随手选一个线上样本跑一下如果结果不对当场就能调整不用再来回找开发排查。6. 一些值得再说的实操心得最后分享几个我在这个项目里学到的经验。第一个是规则语义要尽量贴近业务人员而不是贴近开发。比如“订单金额 100”在代码里可能是一个数字比较但业务人员更习惯看到“订单金额【不低于】100元”。我在界面里把所有操作符都做了中文别名匹配方式用中文展示等于、不等于、大于、大于等于、小于、小于等于、包含、不包含、在列表中、不在列表中。这样即便是不熟悉技术的人理解起来也完全没障碍。第二个是规则的变更要可追溯。虽然我们做了版本管理但时间久了之后业务人员之间容易因为“谁改的、为什么改”产生分歧。后来我在规则发布接口里加了一个变更记录表记录每次发布的规则内容、操作人、操作时间和备注说明。规则汇总展示的时候一眼就能看到最新版本是谁改的、改了什么。第三个是避免过度设计。一开始我想做一个特别灵活通用的规则模型支持任意的函数嵌套、任意的变量组合甚至支持脚本。后来做了一半发现这种灵活性带来的是配置界面的极大复杂化和表达式的不可控。最终我限制住规则模型的表达能力只支持我们业务真正用到的那些条件类型和动作类型界面清爽了维护成本也低了。第四个是做好兜底。规则执行引擎万一崩了不能影响主业务流程。我在客户端调用引擎的外层做了一个 try-catch如果引擎执行抛异常走默认的业务逻辑比如默认不命中、走人工审核同时打日志报警。这个兜底看着简单关键时刻能救命因为规则配置平台的故障不能拖垮主站的交易主链路。这套系统上线到现在规则量已经突破了三千条业务人员每天自己配置和调整规则开发这边基本不再为规则变更占用时间。从技术角度看它不是一个多复杂的系统但确实解决了实际的问题。如果你也在为业务规则频繁变化发愁不妨按这个思路试一试先跑通一个场景再逐步推广。
返回列表