基于Aviator与Spring Boot的动态规则引擎设计实战

基于Aviator与Spring Boot的动态规则引擎设计实战 刚接手一个项目的时候发现里面到处散落着“if orderAmount 1000 userLevel VIP”这种代码每次运营提新活动规则都得改代码、走发布流程、熬夜上线。这种日子我过了很长时间直到后来痛定思痛决定用规则引擎把这块彻底解耦。我选的是Aviator 5.3.3配合Spring Boot做了整套动态规则引擎方案今天把完整设计思路和踩坑过程都写出来希望能给同样被硬编码折磨的同学一个参考。Aviator是一个轻量级的Java表达式求值引擎语法贴近Java依赖少、性能高不像Drools那样重也不像Groovy那样需要额外管理脚本引擎。它解决的核心问题就是把业务规则从代码中抽出来变成可以动态配置、动态刷新的表达式让规则变化不再依赖发版。这套方案特别适合规则经常调整、但又不希望引入重型规则引擎的中小型项目。1. 硬编码的病根与规则引擎的解法1.1 硬编码到底痛在哪里很多项目早期都能靠if-else撑住因为业务简单规则也就两三条。但业务一定会膨胀尤其像营销、风控、计费、任务调度这些系统规则变更频率极高。一旦规则塞进Java代码就必然遇到这几个问题。第一是发布周期长。哪怕只是把折扣从0.8改成0.75也要走完开发、测试、构建、发布全套流程碰上生产环境还要申请变更窗口。第二是测试成本高规则改一次关联的用例都要回归而且规则之间还可能有依赖牵一发动全身。第三是规则不透明业务人员提需求开发人员写代码中间说不清道不明的信息损耗最后线上跑出来的效果跟业务预期对不上排查问题全靠肉眼比对日志。我自己踩过最深的一次坑是晚上十点收到运营消息说大促规则要变了我硬是在公司改代码改到凌晨两点就为一个“满减门槛从300降到200”的改动。那次之后我就下决心以后凡是运营、产品能说清楚的规则绝不让它留在Java代码里。1.2 规则引擎能带来什么改变规则引擎的核心思路是把“规则”和“执行”分离。规则变成一条条可配置的数据存进数据库、配置文件或者配置中心执行引擎负责解释和运行这些规则。业务流程跑的时候从规则库取出匹配的规则动态计算得到结果。这套模式带来的好处非常直接规则修改即时生效不用重新发布应用业务人员可以自己维护简单规则减少开发介入规则集中管理可审计、可追溯代码结构更清爽不再是一坨逻辑堆叠这里要说明一下规则引擎不是银弹它适合的是“规则频繁变化、且规则可以用表达式清晰描述”的场景。如果规则从设计上就属于核心稳定逻辑那老老实实写在代码里反而更好。规则引擎的目的是解决问题不是为了炫技。1.3 方案选型为什么是AviatorJava生态里做规则引擎方案其实不少我当时认真对比过几个主流选项各自特点都比较鲜明。先说Drools。Drools是重资产方案基于Rete算法的规则推理引擎功能非常强大支持复杂的推理链路和决策表适合金融、保险这类规则极其复杂的领域。但它的问题也明显学习曲线陡峭DRL文件语法需要专门培训工程依赖重部署麻烦一个简单项目引入Drools有点杀鸡用牛刀。然后是Easy Rules。老牌轻量库抽象出Rule接口用注解方式定义规则胜在简单。但它的表达式能力其实一般复杂条件组合写起来并不舒服而且它在规则求值这个环节的表达力不如专门的表达式引擎。Groovy脚本也可以做这事JVM上动态执行能力很强但Groovy脚本需要引脚本引擎、管理ClassLoader而且安全问题要花心思处理。我不排斥Groovy但为了几个业务表达式引入一整门动态语言总觉得有点重。最后是Aviator。它是专门为“表达式求值”设计的库语法近似Java学习成本极低团队里任何一个写Java的同事都能直接上手。性能方面Aviator会把表达式编译成字节码再执行同场景下比反射调用的方案快很多。依赖只有一个jar没有额外运行时非常干净。再加上它支持自定义函数、变量绑定、编译缓存做动态规则引擎刚好合适。我当时的结论是如果你的规则场景是“条件判断 数值计算 字符串操作”而不是“复杂推理”Aviator是性价比最高的选择。2. Aviator 5.3.3基础集成与Spring Boot整合2.1 引入依赖与版本选择Aviator的版本变化比较大3.x和5.x的API差异很明显网上的老教程很多是3.x的用法直接搬过来经常会编译报错。我用的是5.3.3版本Maven坐标如下dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.3.3/version /dependency注意5.x版本下group id没有变但artifact包路径和部分API有调整。我一开始用5.0.1版本后来升到5.3.3整体向后兼容做得不错升级没遇到什么大问题。2.2 第一个Aviator表达式先写个最简单的例子感受一下这个库的使用方式。import com.googlecode.aviator.AviatorEvaluator; public class QuickStart { public static void main(String[] args) { // 直接执行表达式结果会自动做类型转换 Long result (Long) AviatorEvaluator.execute(1 2 * 3); System.out.println(result); // 输出 7 // 带变量执行 Double price (Double) AviatorEvaluator.execute(amount * discount, AviatorEvaluator.newEnv(amount, 100.0, discount, 0.8)); System.out.println(price); // 输出 80.0 } }第一段代码输出7第二段输出80.0。看起来就是普通表达式求值但底层做的事情比这个多表达式会先被解析成AST再编译成Java字节码最后执行。因为跳过了每次启动时的解释过程所以性能表现相当不错。有一点要注意Aviator的数值类型处理有自己的一套规则整数默认是Long浮点默认是Double。你在表达式里写“1 / 2”得到的是0而不是0.5因为整数除法在Aviator里和Java一样。需要浮点结果时要么写成“1.0 / 2”要么给变量绑定Double类型值。2.3 在Spring Boot中管理Aviator实例强烈建议不要到处写AviatorEvaluator.execute这种静态调用而是把AviatorEvaluatorInstance作为一个Bean交给Spring管理。这样做有几个好处方便统一配置选项、方便注册自定义函数、方便后续做编译缓存管理。import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.AviatorEvaluatorInstance; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class AviatorConfig { Bean public AviatorEvaluatorInstance aviatorEvaluator() { AviatorEvaluatorInstance instance AviatorEvaluator.getInstance(); // 开启编译缓存避免重复编译相同表达式 instance.setCachedExpressionByDefault(true); // 开启表达式追踪方便排查问题 instance.setTraceable(true); return instance; } }AviatorEvaluator.getInstance()方法默认返回全局单例这么做的好处是Aviator内部的表达式编译缓存是全局共享的相同表达式只会编译一次。你也可以new一个独立的AviatorEvaluatorInstance互相之间缓存隔离适合多租户场景。绝大多数项目用全局单例就够了。这里有个容易忽略的点setCachedExpressionByDefault(true)这个配置非常关键。Aviator执行表达式前会先做编译编译过程有一定开销。开启缓存后相同内容的表达式会命中缓存直接执行字节码性能提升非常明显。这个问题我在后面性能调优的部分还会细说。2.4 自定义表达式校验动态规则意味着表达式可能来自非开发人员安全问题必须重视。Aviator本身是一个安全的表达式求值器默认不提供反射能力杜绝了任意代码执行的风险这比直接用Groovy脚本安全得多。但还是要做一层独立性校验避免有人写了个错表达式导致运行时才炸。import com.googlecode.aviator.lexer.token.OperatorType; import com.googlecode.aviator.AviatorEvaluatorInstance; import com.googlecode.aviator.Expression; public class ExpressionValidator { private final AviatorEvaluatorInstance evaluator; public ExpressionValidator(AviatorEvaluatorInstance evaluator) { this.evaluator evaluator; } public boolean validate(String expression) { try { Expression compiled evaluator.compile(expression); return true; } catch (Exception e) { // 记录日志返回校验失败 return false; } } }把表达式编译一遍能过编译基本就说明语法没问题。运行时还需要考虑变量缺失的问题Aviator默认对不存在的变量会报错但也提供了配置让缺失变量返回null后面我会展开讲。3. 动态规则引擎的核心设计3.1 规则模型怎么设计规则引擎的基础是规则模型。要支撑动态配置模型设计得好不好直接决定扩展性。我当时设计了一张规则表核心字段如下id主键rule_code规则编码业务唯一rule_name规则名称expression规则表达式如“amount minAmount userLevel level”result_type结果类型status启用/停用version版本号create_time / update_time时间戳规则要能灵活配置表达式里的常量参数和变量名都必须有约定。比如规则里用到了minAmount那这个参数可以从规则的属性配置里取而不是写死在表达式里。规则属性的设计如下{ minAmount: 500, discountRate: 0.8, vipLevels: [VIP1, VIP2] }把参数和表达式分开好处是表达式结构稳定业务调整价格只需要改参数不需要动表达式。如果表达式本身也要变再走规则更新流程。3.2 规则存储与加载规则存储我建议优先考虑数据库因为规则一般需要后台管理界面来维护数据库天然方便对接。表结构可以参考下面这个简化版DDLCREATE TABLE dynamic_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_code VARCHAR(64) NOT NULL COMMENT 规则编码, rule_name VARCHAR(128) NOT NULL COMMENT 规则名称, expression TEXT NOT NULL COMMENT 规则表达式, rule_params TEXT COMMENT 规则参数JSON, result_type VARCHAR(32) COMMENT 结果类型, status TINYINT DEFAULT 1 COMMENT 1启用 0停用, version BIGINT DEFAULT 1 COMMENT 规则版本, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_rule_code (rule_code) );加载的时候项目启动时全量加载一次启用中的规则到内存缓存更新规则时通过接口触发缓存刷新。如果项目用了配置中心也可以把规则推到配置中心通过配置中心的监听机制自动刷新。两者思路类似核心是规则不进代码、不进jar包。3.3 动态刷新的实现原理动态规则引擎的灵魂是“改了马上生效”。实现方式通常有两种一种是定时轮询对比版本号一种是事件驱动刷新。我用的是后者简单说就是提供一个刷新接口管理员在后台编辑完规则后调用接口重新加载规则缓存。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/rule) public class RuleController { Autowired private RuleEngineService ruleEngineService; PostMapping(/refresh) public void refresh() { ruleEngineService.reloadRules(); } }reloadRules方法的内部逻辑是重新查一遍数据库的启用规则覆盖内存中的规则缓存。这里注意一个问题多实例部署时只刷新了一个实例的缓存其他实例还是旧规则。所以生产环境建议用Redis发布订阅或者消息队列做广播收到刷新事件后每个实例各自重新加载。如果引入配置中心这个能力是现成的会更省事。3.4 表达式模板与参数分离在实际项目里规则表达式不完全是静态的。比如“订单金额满500减50”和“订单金额满300减30”如果把具体数字写死在表达式里那每改一次金额都要更新表达式记录管理成本高。更好的做法是表达式里使用变量占位参数从上下文动态传入。比如规则表达式统一写成orderAmount minAmount执行的时候把minAmount参数跟业务上下文一起放进运行环境。minAmount的值可能来自用户配置、数据库参数表也可能来自上游接口但表达式的结构始终不变。这种设计把“规则逻辑”和“规则参数”彻底分离后续参数调整完全不用动表达式。我还在表达式的约定里加了一个规矩表达式里出现的变量名必须和上下文对象中的字段名一致团队内部维护一份变量字典避免随手起名导致混乱。4. 实战优惠规则引擎从0到14.1 业务场景描述我拿一个电商优惠计算场景来串起完整流程。需求是这样系统要根据用户会员等级、订单金额、商品品类计算最终优惠金额规则可以随时调整。运营希望通过后台配置就能改变活动规则而不是每次找开发发版。大概拆解一下核心规则普通用户订单满200减20满500减60VIP用户订单满200减30满500减100且享受9.5折叠加生鲜品类不参与满减每个用户每日最多享受3次优惠这些规则用if-else写出来少说得有二三十行而且每一行都是潜在的变更点。用Aviator表达式配参数之后每条规则就是一行表达式加一组参数清晰干净。4.2 定义规则上下文对象规则执行时所有业务数据都要以变量的形式传给表达式引擎。我定义了一个上下文对象把可能用到的数据都放进去。import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Set; Data public class OrderContext { private String orderId; private BigDecimal orderAmount; private String userLevel; private Long userId; private String category; private Integer usedCouponCount; private LocalDateTime orderTime; private SetString excludedCategories; }注意BigDecimal类型的处理。Aviator对BigDecimal是支持的但比较时要注意精度问题在表达式里使用“”对BigDecimal不适用要用内置函数或者转为double比较。我的做法是在放入上下文之前把BigDecimal统一转为double值避免精度比较的坑。但涉及金额计算时double会丢失精度所以我只在规则判断这个环节用double最终金额计算还是在Java代码里用BigDecimal完成。4.3 规则执行核心代码规则引擎的核心执行逻辑绑定表达式 绑定上下文 返回结果。import com.googlecode.aviator.AviatorEvaluatorInstance; import com.googlecode.aviator.Expression; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; import java.util.Map; Service public class RuleEngineService { Autowired private AviatorEvaluatorInstance aviatorEvaluator; private ListDynamicRule ruleCache; private volatile long version; public boolean evaluate(String ruleCode, OrderContext context) { DynamicRule rule getRuleFromCache(ruleCode); if (rule null) { throw new IllegalArgumentException(rule not found: ruleCode); } // 编译并缓存表达式 Expression expression aviatorEvaluator.compile(rule.getExpression()); MapString, Object env buildEnv(rule, context); return Boolean.TRUE.equals(expression.execute(env)); } private MapString, Object buildEnv(DynamicRule rule, OrderContext context) { MapString, Object env new HashMap(); env.put(orderAmount, context.getOrderAmount().doubleValue()); env.put(userLevel, context.getUserLevel()); env.put(category, context.getCategory()); env.put(usedCouponCount, context.getUsedCouponCount()); // 把规则参数也放入环境 env.putAll(rule.getParams()); return env; } }这里有几个细节值得说一下。第一rule.getParams()返回的是规则配置参数的Map里面的key如果和上下文key冲突以param为准这个规则要在文档里写清楚。第二compile之后如果开启了缓存重复执行不会重新编译放心调用。第三返回值统一用Boolean.TRUE.equals包裹避免表达式直接返回null导致NPE。4.4 规则组合多条规则如何协同真实业务里很少只有一条规则。“满减 会员折扣 品类排除”常常是叠加的。我实现了一个规则集执行器按优先级顺序执行规则集合最终结果合并。public MapString, Object executeRuleSet(String ruleSetCode, OrderContext context) { ListDynamicRule rules getRulesBySetCode(ruleSetCode); MapString, Object result new HashMap(); for (DynamicRule rule : rules) { // 命中规则则叠加结果 if (evaluate(rule.getRuleCode(), context)) { applyRuleEffect(result, rule); } } return result; }规则集的设计需要注意优先级和互斥关系。比如“满500减60”和“满500减100”不应该同时命中那就得在规则集里配好优先级或者用“命中即中断”的策略。我通常用规则优先级字段数值越小优先级越高执行时排序后逐条判断。对于互斥规则先命中的生效后续同组规则跳过。对于叠加规则全部命中依次作用。4.5 把这个能力变成线上可维护的系统代码写完后还有一个重要环节让规则具备线上可视化维护能力。我的做法是整理一个极简配置后台支持规则的新增、编辑、停用和测试。测试功能很关键。后台可以输入一个模拟的订单上下文JSON点击执行看表达式结果这样业务人员自己就能验证规则是否符合预期不需要拉上开发陪跑。这个设计虽然简单但实际使用中特别受欢迎很大程度降低了运营同学对规则引擎的抗拒感。平台建设的重心排序建议是表达式校验先行、上下文变量字典次之、可视化编辑最后。不要一上来就想做大而全的规则平台先把核心流程跑通再逐步补周边能力。5. 项目应用扩展工时与进度管理场景规则引擎不止能算优惠券任何“条件判断 计算”的业务都能套用。我后来在基于Spring Boot的进度跟踪与工时管理项目里也用了这套引擎场景完全不同但思路完全一致。这个场景的需求是员工填报工时后系统要根据项目进度、任务类型、加班时长、人员职级等条件自动判断工时是否合理、是否触发超时预警、是否进入审批流。规则同样频繁变化公司每季度调整工时标准、不同项目组有不同的加班阈值、特殊项目还有单独的豁免逻辑。如果这些判断全部写死在Java代码里每个季度来一次变更开发基本不用干别的了。我用同样的方案把工时判断规则配置化效果非常明显。举个例子表达式taskType development actualHours planHours * 1.2 projectPhase critical返回结果触发预警通知项目经理这类规则在代码里写改一次成本其实不高但架不住数量多、变化频繁。十几个判断逻辑分布在各个Service里每次改完还要担心漏改关联逻辑。抽到规则引擎后所有判断口径集中在规则表里口径统一、改动可控。另外一个隐藏价值是审计。规则变化有记录谁在什么时间改了什么规则一目了然。这在代码时代几乎做不到因为代码变更和业务规则的对应关系全靠人脑记忆。有朋友问我这类项目进度管理的系统规则逻辑本身不算复杂直接用枚举加配置不是也行吗确实可以但如果你的规则组合维度多、交集复杂配置项会膨胀得非常快。比如职级、项目类型、任务类型、紧急程度、时间段这五个维度组合可能的判断路径是指数级的。规则引擎把维度判断用表达式组织起来维护成本要低一个量级。6. 性能优化与风险控制6.1 表达式编译缓存Aviator性能相关的配置最主要的就是编译缓存。来看一个实测场景某个接口每秒钟要执行上百次规则判断表达式内容高度重复。如果不开启缓存每次执行都要重新解析和编译表达式性能损耗非常明显。开启缓存后编译结果直接复用执行耗时能降到微秒级别。一个容易踩的坑如果你动态拼接表达式每次拼出来的字符串都略有不同那缓存就形同虚设。比如用字符串拼接把参数值拼进表达式而不是用变量方式传入表达式会随着参数值的不同而不断产生新内容缓存无法命中这和前面强调的“表达式与参数分离”原则是呼应的。注意Aviator的缓存机制是按表达式原始字符串匹配的字符串一变就是新表达式。想让缓存命中率高表达式必须保持结构稳定变化的都通过变量和参数传入。6.2 并发场景下的线程安全AviatorEvaluatorInstance是线程安全的多线程并发执行表达式不需要额外加锁。表达式编译过程也是线程安全的多个线程同时提交同一个表达式内部会有锁机制避免重复编译。我在高并发场景下测试过100个线程并发执行同一表达式内存和CPU都很稳定没有出现线程安全问题。不过要注意如果你在自定义函数里维护了共享状态比如一个计数变量那线程安全就得自己负责了。自定义函数最好设计成无状态函数需要状态的场景用线程安全的容器。6.3 表达式安全与异常兜底动态规则来自外部再怎么强调合法性校验都不为过。我总结了几条安全红线第一Aviator默认本身不具备任意代码执行能力这比直接跑Groovy脚本安全得多但并不意味着完全不用管。表达式里不能访问任意Java类一旦有人注入了反射相关函数风险等级就完全不同了。第二编译表达式时要做异常捕获。Aviator编译异常会抛出异常类型包括语法错误和变量类型不匹配必须在校验阶段拦截。第三执行阶段也要做兜底。比如规则调用自定义函数内部出错或者上下文数据缺失都会导致执行异常。不能因为某条规则出错就把整个业务请求拖垮。我做的方案是规则执行器包一层try-catch规则执行失败时记录错误日志然后走默认策略比如默认放行或者默认拒绝确保主流程稳定。public boolean safeEvaluate(String ruleCode, OrderContext context) { try { return evaluate(ruleCode, context); } catch (Exception e) { log.error(rule execute error, ruleCode: {}, context: {}, ruleCode, context, e); // 规则执行失败返回默认值不阻塞业务主流程 return Boolean.FALSE; } }第四规则要限制执行时间。Aviator表达式一般执行很快微秒到毫秒级但万一某个表达式设计不当比如死循环或者数据量巨大可能拖住线程。可以在执行时配置超时时间超时后中断执行避免影响整体吞吐。6.4 规则灰度发布与回滚规则变更直接作用于生产环境风险还是要控制的。我做了一个简单的灰度策略新规则先以“测试中”状态发布只对测试流量生效验证通过后切成“灰度”状态对部分真实流量生效最终才完全放量。回滚方面因为规则表有version字段每次更新都会生成新版本出问题只需要把状态改回去或者把版本号切到上一个版本。这个能力和数据库的乐观锁思想差不多实现成本不高但线上出问题时非常救命。7. 常见问题与排查技巧实录7.1 表达式编译报错与环境不匹配Aviator 5.x和3.x接口差异不小网上老教程很容易误导。常见的坑包括getInstance方法变了、execute的返回类型变了、一些内置函数名称调整。遇到编译报错优先确认依赖版本然后对照5.x的官方文档检查API用法。另外要注意Aviator的算术运算结果类型遵循一定的规则整数除法返回整数参与运算的变量类型不一致时会做自动提升。如果发现计算结果和预期不符先检查表达式里的数值字面量类型。7.2 变量为null导致的执行异常Aviator对null的处理比较严格。如果表达式里访问的变量没有在环境中绑定默认会抛出异常。在规则引擎场景里这个行为可能会导致一个参数没传整条规则炸掉。有两种处理方式。一种是在构建env时确保所有可能用到的变量都有默认值另一种是给Aviator配置null变量处理策略。我倾向于前者显式地控制上下文边界同时在校验阶段检查表达式中引用的变量名是否都存在于变量字典里。7.3 Spring Boot启动不显示端口号问题有些同学在Spring Boot项目里集成Aviator后启动日志里看不到Tomcat端口号以为是冲突了。其实跟Aviator没直接关系多数情况是日志配置把启动信息打到了别的地方或者启动成功日志被覆盖。排查思路很简单看是否真正启动成功访问接口看通不通检查logback的日志级别把org.springframework.boot组的日志级别调到INFO。7.4 MyBatis-Plus查询规则列表慢规则管理后台如果用MyBatis-Plus规则表数据量大了之后查询会变慢。我这里分享一个配置经验如果你的Mapper XML文件和Mapper接口放在同一个目录下需要在pom.xml里加配置让Maven把XML文件也打包到classpath中。否则本地跑得通打包部署后规则查询就会报找不到SQL。build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build这个坑比较隐蔽因为我本地IDEA跑的时候一切正常到了服务器上就是“Invalid bound statement”排查了很久才发现是打包配置的问题。7.5 规则引擎的日志与Debug技巧规则引擎是个黑盒规则不对很难直观看出问题。我的实践是开启Aviator的trace输出表达式执行时会打印详细的变量求值过程和中间步骤。生产环境不建议长期开排查问题时临时打开用完就关。另外我会把表达式的入参变量和最终执行结果都记录到日志格式统一。规则出问题时拿着规则编码和上下文参数能很快定位到是表达式逻辑问题还是数据问题。如果上下文数据本身有问题表达式写得再对也是错。提示规则执行日志至少要包含规则编码、规则版本、关键入参、执行结果以及执行耗时。问题排查时第一条原则就是先复现日志信息足够丰富才能快速复现和定位。8. 经验总结我看好但你需要谨慎的地方我用了Aviator做动态规则引擎之后最大的体感是运营改规则终于不用再找我了。这种“被解放”的感觉确实让人上瘾。但冷静下来我必须说几句真话。第一规则引擎不是越早引入越好。如果你的规则半年都不变一次数量就两三条用if-else完全没问题没必要为了“优雅”去引入额外的复杂度。技术方案不能脱离业务场景谈优劣。第二动态意味着失控风险。规则从代码里抽出来好处是灵活坏处是任何人都可能改出问题。必须有完善的校验机制、审计机制和回滚机制。规则越开放管控的成本就越高。没有配套治理措施之前别急着把规则开放给业务人员。第三Aviator是非常优秀的表达式引擎但它不负责规则依赖、规则冲突、规则闭环这些事情。你要做的动态规则引擎本质上是围绕Aviator搭一套规则管理体系和执行治理体系。Aviator解决的是“怎么算”而规则管理的核心是“怎么管”。最后分享一个我用得最多的模式把规则拆成“规则定义”和“规则参数”两层。表达式保持精简稳定参数从配置中心或数据库动态读取。大部分规则变更根本不涉及表达式变化业务体验是改个参数立刻生效而且因为表达式稳定编译缓存的命中率也高兼顾了使用体验和性能。这个模式看着简单实际效果却比一开始预想的好得多。如果你现在的项目正被硬编码规则困扰可以先拿一个非核心业务试试这套方案跑通一条完整的链路再逐渐铺开。规则引擎不是银弹但它确实能让你的代码和业务规则之间多出一道真正灵活的缓冲层。