资讯动态

从 if-else 到声明式规则引擎:手写一个 Lemma 风格 DSL

发布时间:2026/8/28 14:21:15 来源:尧图企业网站定制
在公司里做业务需求的时候不知道你有没有遇到过这种场景产品经理提了一条规则例如“新用户首单满 100 减 20但只能使用一次”开发同学在 Service 层写了一大段 if-else过了一周规则改成“满 150 减 30老用户也能参与”于是又开始改代码、发版、回归测试。规则稍微复杂一点逻辑分支就像滚雪球一样膨胀最后没人敢动那一段代码。这类问题的本质是业务规则被硬编码在了命令式语言里。程序语言擅长描述“怎么算”但业务人员真正关心的是“算什么”和“什么时候生效”。当规则频繁变化时我们希望规则本身可以独立于主流程存在用一种更接近自然语言的、声明式的方式去描述然后由引擎去解释和执行。这也就是Lemma这类声明式业务规则语言declarative language for business rules出现的理由。本文会从概念入手讲清楚 Lemma 和传统规则引擎的差异然后带大家亲手实现一个最小可运行的声明式规则语言 Demo。不需要多高深的理论跟着步骤走你也能在自己的项目里落地一套轻量的规则语言。1. 背景与核心概念1.1 什么是 Lemma简单说Lemma 是一门面向业务规则的声明式语言。这里的“声明式”指的是你只需要描述规则是什么而不用关心规则引擎底层是怎么遍历、怎么匹配、怎么执行的。举个例子传统命令式代码是这样的if (order.getAmount() 100 user.isNew()) { order.setDiscount(20); }这段代码没有错但它把规则和业务逻辑混在了一起。如果规则数量上升到几十条、上百条整个方法会变得难以阅读和维护。用声明式规则语言表达同样的逻辑大致是这样的风格rule 新用户首单满减 when user.new true order.amount 100 then order.discount 20 end你一眼就能看出这条规则在说什么新用户、订单满 100减 20。规则本身就是业务人员可以读懂的描述而不是一堆嵌套的分支判断。Lemma 的定位就是提供这样一套标准化的规则表达方式同时内置执行引擎让规则可以被加载、解析、匹配并执行。它把“业务规则的编写”和“业务规则的执行”解耦从而提升业务迭代速度。1.2 它解决什么问题先看几个典型的业务痛点第一个痛点规则散落在代码里同一个业务规则可能分布在 Controller、Service、甚至 SQL 查询里。规则一旦变化就需要同时修改多处代码很容易漏改。第二个痛点规则变化频繁营销活动、风控策略、审批流程这些业务场景的规则变化非常快。如果每次都走需求评审、开发、测试、发版的完整流程业务效率会非常低。有了声明式规则语言规则可以配置化甚至允许业务人员直接维护。第三个痛点规则之间互相影响现实中的规则很少是孤立的。例如“新人优惠”和“满减活动”可能同时生效也可能互相排斥。用 if-else 表达这类约束需要非常小心地控制优先级和流程分支而声明式规则引擎通常内置了冲突处理、优先级排序、规则关联等能力。1.3 和规则引擎、DSL 的关系很多人会问Lemma 和 Drools、Easy Rules 这类规则引擎有什么不同从定位上看规则引擎是一个更宽泛的概念它负责规则的管理和执行Lemma 则侧重于“语言”本身的设计。实际上一个完整的规则系统由两部分组成DSL领域特定语言例如 Lemma 语言用来描述规则。规则引擎解析 DSL 并执行规则例如 Drools、Easy Rules或者你自己实现的轻量引擎。所以你可以把 Lemma 理解为一种“规则描述语言”它需要配合执行环境一起使用。也有一些语言试图把两者统一比如 Prolog 其实就是一种可以运行在逻辑编程环境中的声明式语言。Lemma 的侧重点是面向业务规则它不像 Prolog 那样强调逻辑推理而是更关注规则的条件匹配和动作执行。1.4 为什么需要掌握这种思想再退一步想即使你最后没有用 Lemma掌握声明式规则表达的思想也很有价值。它能帮助你在设计业务系统时把易变的规则从稳定代码中分离出来。用更少的代码承载更多的业务变化减少发版频率。让业务规则变得可读、可审计、可测试。所以这篇教程不仅会介绍 Lemma 是什么更重要的是带读者理解声明式规则语言的设计思路和落地方法。2. 环境准备与项目结构为了后面能动手实践我们先准备好开发环境。本节的示例会采用 Java Spring Boot 实现一个最小的 Lemma 风格规则引擎你可以把它理解成一个“玩具版本”但足够演示声明式规则的核心思想。2.1 运行环境本文示例基于以下环境JDK 1.8 或以上版本Maven 3.6 或以上版本Spring Boot 2.7.x低版本兼容性更好高版本同样适用IDE 推荐 IntelliJ IDEA如果你的环境版本不一样也不用担心本文的核心代码不依赖太多第三方库你可以在任意 Java 环境中运行。2.2 项目依赖首先创建一个 Spring Boot 项目或者直接创建普通的 Maven 项目也可以。为了演示方便我们引入最少的依赖dependencies !-- Web 依赖方便用接口测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 用 fastjson 做 JSON 解析非必须实际可按需引入 -- dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version1.2.83/version /dependency /dependencies注意这里使用 fastjson 只是为了简化 JSON 对象处理实际项目中你也可以用 Jackson 或 Gson。核心逻辑不依赖具体 JSON 库。2.3 示例项目结构我们在项目中计划实现一个非常精简的声明式规则引擎命名为lemma-demo。目录结构如下lemma-demo ├── pom.xml └── src/main/java/com/example/lemma ├── LemmaDemoApplication.java ├── model │ ├── Order.java │ └── User.java ├── rules │ ├── Rule.java │ ├── RuleCondition.java │ ├── RuleAction.java │ └── RuleParser.java ├── engine │ ├── RuleEngine.java │ └── RuleContext.java └── controller └── RuleController.java这个结构比较典型model包存放业务模型也就是规则要操作的对象。rules包定义规则的数据结构和解析器。engine包实现规则匹配和执行的引擎。controller包提供 REST 接口方便测试。后面我们会逐步填充每一个文件。3. 声明式规则语言的核心设计拆解在写代码之前先想清楚一个问题一个最小可用的声明式规则语言需要具备哪些能力3.1 规则的基本结构任何规则语言哪怕再复杂最终都逃不开这样一个结构rule [规则名] when [条件列表] then [动作列表] end条件列表和动作列表是规则的核心。条件描述的是“什么情况下这条规则生效”。动作描述的是“生效之后要做什么”。为了不让引擎过于复杂我们可以先把条件限定为最简单的表达式形式字段名 比较符 值例如user.new true order.amount 100动作也可以先限制为“赋值”操作order.discount 20这种设计虽然简单但已经可以表达相当多的业务规则了。3.2 比较符与逻辑组合有了基本结构下一步要支持哪些比较符等于!不等于大于大于等于小于小于等于这些比较符覆盖了绝大多数业务场景。在表达更复杂的规则时可能还需要and、or逻辑组合。在最小实现中我们可以约定同一规则中的多个条件之间默认是“并且”的关系。这样可以用最简单的数据结构表达。如果一个规则需要“或者”关系可以拆分成两条规则或者后续扩展条件类型。在最小 Demo 阶段保持简单更重要。3.3 规则解析器的职责规则解析器负责把一段字符串形式的规则文本转换为内存中的Rule对象。解析器是声明式规则语言的关键部分因为业务人员写的是文本引擎执行的是对象。一个比较清晰的规则文本格式可以参考下面这个例子rule 新人满减 when user.new true order.amount 100 then order.discount 20 end解析器读取这段文本后需要识别规则名。提取 when 块中的条件行。提取 then 块中的动作行。分别封装成RuleCondition和RuleAction列表。3.4 引擎的执行流程引擎的执行流程比较固定可以拆成三步加载规则把规则文本解析成Rule对象列表。匹配规则针对传入的规则上下文包含订单、用户等业务对象逐条判断条件是否成立。执行动作对匹配成功的规则执行动作列表中的赋值操作。打个比方这就像是在做一次过滤规则库里有几十条规则引擎拿到一个具体业务场景后筛选出其中所有“当前场景适用”的规则然后逐一执行它们的结果。理解了这些核心概念后下面就可以动手写代码了。4. 完整实战实现一个最小的 Lemma 风格规则引擎这一节我们来实现一个可运行的 Demo。虽然代码量不大但已经能够完整体现“声明式规则语言”从文本到执行的完整链路。4.1 定义业务模型我们先创建两个业务模型类用户和订单。文件路径src/main/java/com/example/lemma/model/User.javapackage com.example.lemma.model; public class User { private String name; private boolean newUser; private int level; public User(String name, boolean newUser, int level) { this.name name; this.newUser newUser; this.level level; } public String getName() { return name; } public void setName(String name) { this.name name; } public boolean isNewUser() { return newUser; } public void setNewUser(boolean newUser) { this.newUser newUser; } public int getLevel() { return level; } public void setLevel(int level) { this.level level; } }这里要注意属性名我们叫newUser因为规则文本中我们约定用user.newUser来引用。你也可以叫new但new是 Java 关键字Java Bean 的 getter 命名会冲突所以这里用newUser更稳妥。文件路径src/main/java/com/example/lemma/model/Order.javapackage com.example.lemma.model; import java.math.BigDecimal; public class Order { private String orderNo; private BigDecimal amount; private BigDecimal discount BigDecimal.ZERO; public Order(String orderNo, BigDecimal amount) { this.orderNo orderNo; this.amount amount; } public String getOrderNo() { return orderNo; } public void setOrderNo(String orderNo) { this.orderNo orderNo; } public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount amount; } public BigDecimal getDiscount() { return discount; } public void setDiscount(BigDecimal discount) { this.discount discount; } }这两个模型足够用了。实际项目中你会有更复杂的属性但基本原则是一样的。4.2 实现规则结构与解析器接下来定义规则的数据结构。文件路径src/main/java/com/example/lemma/rules/RuleCondition.javapackage com.example.lemma.rules; public class RuleCondition { private String field; private String operator; private String value; public RuleCondition(String field, String operator, String value) { this.field field; this.operator operator; this.value value; } public String getField() { return field; } public String getOperator() { return operator; } public String getValue() { return value; } }文件路径src/main/java/com/example/lemma/rules/RuleAction.javapackage com.example.lemma.rules; public class RuleAction { private String field; private String value; public RuleAction(String field, String value) { this.field field; this.value value; } public String getField() { return field; } public String getValue() { return value; } }这两个类非常简单本质上就是两个数据容器解析器把规则文本填充进去引擎再读取它们。文件路径src/main/java/com/example/lemma/rules/Rule.javapackage com.example.lemma.rules; import java.util.ArrayList; import java.util.List; public class Rule { private String name; private ListRuleCondition conditions new ArrayList(); private ListRuleAction actions new ArrayList(); public String getName() { return name; } public void setName(String name) { this.name name; } public ListRuleCondition getConditions() { return conditions; } public void setConditions(ListRuleCondition conditions) { this.conditions conditions; } public ListRuleAction getActions() { return actions; } public void setActions(ListRuleAction actions) { this.actions actions; } }有了这几个类之后解析器就可以开工了。文件路径src/main/java/com/example/lemma/rules/RuleParser.javapackage com.example.lemma.rules; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class RuleParser { private static final Pattern FIELD_OP_VALUE Pattern.compile( ^(\\w\\.\\w)\\s*(|!||||)\\s*(.)$ ); public ListRule parse(String ruleText) { ListRule rules new ArrayList(); // 按 rule 关键字切分 String[] sections ruleText.split((?rule\\s\)); for (String section : sections) { try { Rule rule parseRule(section); if (rule ! null) { rules.add(rule); } } catch (IllegalArgumentException e) { System.err.println(解析规则失败 section); e.printStackTrace(); } } return rules; } private Rule parseRule(String section) { if (section null || section.trim().isEmpty()) { return null; } Rule rule new Rule(); // 解析规则名 Matcher nameMatcher Pattern.compile(rule\\s\([^\])\).matcher(section); if (!nameMatcher.find()) { throw new IllegalArgumentException(未找到规则名); } rule.setName(nameMatcher.group(1)); // 分离 when 和 then 块 String whenBlock extractBlock(section, when); String thenBlock extractBlock(section, then); // 解析条件 String[] conditionLines whenBlock.split(\n); for (String line : conditionLines) { String conditionText line.trim(); if (conditionText.isEmpty()) { continue; } Matcher matcher FIELD_OP_VALUE.matcher(conditionText); if (matcher.find()) { rule.getConditions().add(new RuleCondition( matcher.group(1), matcher.group(2), matcher.group(3).trim() )); } else { throw new IllegalArgumentException(无法解析条件: conditionText); } } // 解析动作 String[] actionLines thenBlock.split(\n); for (String line : actionLines) { String actionText line.trim(); if (actionText.isEmpty()) { continue; } // 动作格式为字段 值 String[] parts actionText.split(); if (parts.length 2) { rule.getActions().add(new RuleAction( parts[0].trim(), parts[1].trim() )); } else { throw new IllegalArgumentException(无法解析动作: actionText); } } return rule; } private String extractBlock(String section, String keyword) { int startIndex section.indexOf(keyword); if (startIndex 0) { throw new IllegalArgumentException(缺少 keyword 块); } int contentStart section.indexOf(\n, startIndex); if (contentStart 0) { throw new IllegalArgumentException(keyword 块缺少内容); } int endIndex; if (when.equals(keyword)) { endIndex section.indexOf(then, contentStart); } else { endIndex section.indexOf(end, contentStart); } if (endIndex 0) { throw new IllegalArgumentException(keyword 块缺少结束标记); } return section.substring(contentStart, endIndex); } }这个解析器做了一些简化处理用正则表达式匹配字段 操作符 值格式。按when和then关键字切分文本块。条件行和动作行均按换行符切分。在实际项目中你可能需要更鲁棒的解析方案比如使用 ANTLR 定义词法和语法但作为教学示例这个解析器已经足够展示核心思路。4.3 实现规则上下文与引擎解析器把文本转换为Rule对象之后引擎需要能够读取上下文中对象的具体属性值。这里我们借助反射来实现属性的读取和赋值。文件路径src/main/java/com/example/lemma/engine/RuleContext.javapackage com.example.lemma.engine; import java.util.HashMap; import java.util.Map; public class RuleContext { private MapString, Object variables new HashMap(); public void setVariable(String name, Object value) { variables.put(name, value); } public Object getVariable(String name) { return variables.get(name); } }这是最简单的上下文实现用一个 Map 存放变量引用。user和order这样的对象会注册进去。文件路径src/main/java/com/example/lemma/engine/RuleEngine.javapackage com.example.lemma.engine; import com.example.lemma.rules.Rule; import com.example.lemma.rules.RuleAction; import com.example.lemma.rules.RuleCondition; import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.List; public class RuleEngine { public void execute(ListRule rules, RuleContext context) { for (Rule rule : rules) { if (match(rule, context)) { apply(rule, context); } } } private boolean match(Rule rule, RuleContext context) { for (RuleCondition condition : rule.getConditions()) { if (!matchesCondition(condition, context)) { return false; } } return true; } private boolean matchesCondition(RuleCondition condition, RuleContext context) { String field condition.getField(); Object actualValue resolveField(field, context); if (actualValue null) { return false; } String operator condition.getOperator(); String expectedValueStr condition.getValue(); // 特殊处理 Boolean 比较 if (actualValue instanceof Boolean) { boolean actual (Boolean) actualValue; boolean expected Boolean.parseBoolean(expectedValueStr); if (.equals(operator)) { return actual expected; } else if (!.equals(operator)) { return actual ! expected; } return false; } // 数字比较 if (actualValue instanceof Number) { BigDecimal actual new BigDecimal(actualValue.toString()); BigDecimal expected new BigDecimal(expectedValueStr); int compareResult actual.compareTo(expected); switch (operator) { case : return compareResult 0; case !: return compareResult ! 0; case : return compareResult 0; case : return compareResult 0; case : return compareResult 0; case : return compareResult 0; default: return false; } } // 字符串比较 if (actualValue instanceof String) { int compareResult actualValue.toString().compareTo(expectedValueStr); switch (operator) { case : return compareResult 0; case !: return compareResult ! 0; case : return compareResult 0; case : return compareResult 0; case : return compareResult 0; case : return compareResult 0; default: return false; } } return false; } private void apply(Rule rule, RuleContext context) { for (RuleAction action : rule.getActions()) { String field action.getField(); String value action.getValue(); setFieldValue(field, value, context); } } private Object resolveField(String field, RuleContext context) { String[] parts field.split(\\.); if (parts.length ! 2) { throw new IllegalArgumentException(字段格式应该为 变量.属性: field); } Object target context.getVariable(parts[0]); if (target null) { throw new IllegalArgumentException(上下文中未找到变量: parts[0]); } try { Field f target.getClass().getDeclaredField(parts[1]); f.setAccessible(true); return f.get(target); } catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalArgumentException(无法读取属性: field, e); } } private void setFieldValue(String field, String value, RuleContext context) { String[] parts field.split(\\.); if (parts.length ! 2) { throw new IllegalArgumentException(字段格式应该为 变量.属性: field); } Object target context.getVariable(parts[0]); if (target null) { throw new IllegalArgumentException(上下文中未找到变量: parts[0]); } try { Field f target.getClass().getDeclaredField(parts[1]); f.setAccessible(true); Class? fieldType f.getType(); if (fieldType BigDecimal.class) { f.set(target, new BigDecimal(value)); } else if (fieldType boolean.class || fieldType Boolean.class) { f.set(target, Boolean.parseBoolean(value)); } else if (fieldType int.class || fieldType Integer.class) { f.set(target, Integer.parseInt(value)); } else if (fieldType String.class) { f.set(target, value); } else { throw new IllegalArgumentException(暂不支持的类型: fieldType.getName()); } } catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalArgumentException(无法设置属性: field, e); } } }引擎的核心逻辑集中在matchesCondition和apply两个方法中matchesCondition根据操作符类型对布尔、数字、字符串分别做比较。apply通过反射把规则中声明的值赋给目标字段。要注意BigDecimal的比较不能用equals因为它是按精度比较的1.0和1.00的equals结果是false。这里用compareTo更合理。4.4 编写启动类与测试接口把以上组件串起来之后我们写一个启动类和一个测试接口。文件路径src/main/java/com/example/lemma/LemmaDemoApplication.javapackage com.example.lemma; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class LemmaDemoApplication { public static void main(String[] args) { SpringApplication.run(LemmaDemoApplication.class, args); } }文件路径src/main/java/com/example/lemma/controller/RuleController.javapackage com.example.lemma.controller; import com.example.lemma.engine.RuleContext; import com.example.lemma.engine.RuleEngine; import com.example.lemma.model.Order; import com.example.lemma.model.User; import com.example.lemma.rules.Rule; import com.example.lemma.rules.RuleParser; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.List; import java.util.Map; RestController public class RuleController { private final RuleParser parser new RuleParser(); private final RuleEngine engine new RuleEngine(); PostMapping(/execute) public MapString, Object execute(RequestBody MapString, Object request) { String ruleText (String) request.get(ruleText); Boolean userNew (Boolean) request.get(userNew); Integer userLevel (Integer) request.get(userLevel); BigDecimal orderAmount new BigDecimal(request.get(orderAmount).toString()); // 构造业务对象 User user new User(test, userNew ! null userNew, userLevel null ? 0 : userLevel); Order order new Order(NO001, orderAmount); // 准备上下文 RuleContext context new RuleContext(); context.setVariable(user, user); context.setVariable(order, order); // 解析并执行规则 ListRule rules parser.parse(ruleText); engine.execute(rules, context); return Map.of( orderNo, order.getOrderNo(), discount, order.getDiscount(), matchedRules, rules.size() ); } }这里用了一个非常简单的POST接口来做验证。实际项目中规则文本通常存储在数据库或配置中心而不是请求参数里。4.5 运行与验证启动 Spring Boot 应用后我们可以用curl或 Postman 发送请求来验证。测试用例一新用户订单金额 120期望命中规则。curl -X POST http://localhost:8080/execute \ -H Content-Type: application/json \ -d { ruleText: rule \新人满减\\nwhen\n user.newUser true\n order.amount 100\nthen\n order.discount 20\nend, userNew: true, userLevel: 0, orderAmount: 120 }预期结果{ orderNo: NO001, discount: 20, matchedRules: 1 }测试用例二非新用户订单金额 120不命中规则。curl -X POST http://localhost:8080/execute \ -H Content-Type: application/json \ -d { ruleText: rule \新人满减\\nwhen\n user.newUser true\n order.amount 100\nthen\n order.discount 20\nend, userNew: false, userLevel: 1, orderAmount: 120 }预期结果{ orderNo: NO001, discount: 0, matchedRules: 0 }到这里一个最小的声明式规则引擎就跑通了。你可能已经注意到整个执行链路非常清晰规则文本 - 解析器 - Rule 对象 - 引擎匹配 - 动作执行。5. 用规则语言表达真实业务场景光有 Demo 还不够我们来模拟几个更贴近线上的业务场景验证这种规则表达方式的灵活性。5.1 场景一会员等级折扣假设系统有一条规则只有 3 级及以上的会员在下单金额超过 500 元时可以享受 50 元优惠。规则文本可以写成rule 高等级会员满减 when user.level 3 order.amount 500 then order.discount 50 end如果后续活动改成“等级降到 2 级”只需要修改规则文本不需要改代码。5.2 场景二非新用户不参与首单优惠再来看一个稍微复杂一点的场景如果系统同时有“新人优惠”和“会员折扣”两条规则就涉及一个优先级问题。比如我们希望“新人优惠”只对首单用户生效会员折扣不叠加。一种最简单的处理方式是把这些规则拆开写rule 首单优惠 when user.newUser true order.amount 100 then order.discount 20 end rule 会员折扣 when user.newUser false user.level 3 order.amount 500 then order.discount 50 end这样写的好处是每条规则都比较短业务人员也容易理解“什么情况下走哪个优惠”。如果你需要更复杂的互斥、优先级处理就需要在引擎层加入规则冲突处理策略例如salience优先级字段。这在 Drools 等成熟规则引擎中很常见但对最小实现来说可以先不涉及。5.3 场景三风控规则风控场景也非常适合声明式规则语言。例如rule 高频下单拦截 when user.level 0 order.amount 10000 then order.discount 0 end这里的动作可能不是给折扣而是设置一个风控标记。你可以把动作扩展成任意字段赋值比如order.riskFlag 1。从这些例子可以看出声明式规则语言的表达能力取决于你定义的字段、操作符和动作。字段越贴合业务规则就越容易读。6. 常见问题与排查思路在实际动手实现或使用声明式规则语言时有几个问题经常出现。这里整理一份问题排查清单。6.1 常见问题汇总问题现象常见原因解决思路规则解析失败规则文本中when或then缺少换行检查规则文本格式确认关键字后面跟的是换行符条件永远不匹配属性名拼写错误或反射读取不到属性检查字段中的变量名和属性名是否一致比较结果不对数字类型混用浮点数用equals比较优先用BigDecimal.compareTo比较数字动作赋值不生效字段名写错或类型转换失败检查动作行中的字段是否存在类型是否受支持多条规则互相覆盖规则执行顺序混乱设计优先级或冲突处理策略规则数量多了以后性能差每条规则都走反射遍历缓存反射结果引入索引或先粗糙筛选候选规则6.2 属性读取不到怎么办如果你执行规则时遇到无法读取属性异常可以按以下步骤排查确认规则文本中的变量名是user、order这样的注册名。确认变量名后的属性名是 Java 类中真实存在的字段名。检查该字段是否是private引擎中已经调用了setAccessible(true)如果不是这个原因检查字段是否被static修饰。确认字段名没有拼写错误比如newUser不要写成newuser。6.3 规则优先级不明确最小实现是“按规则集合的顺序依次执行”这在实际项目中可能不够用。通常的解决方案有两种给Rule增加一个priority字段在执行前按优先级排序。匹配成功后立即中断只执行第一条匹配的规则。具体选择哪种方案取决于业务需求。比如营销场景通常需要优惠叠加就应该全部执行而风控场景通常只摘取最高优先级的结果就应该中断。6.4 如何保证规则的可测试性规则代码分散在 If-Else 中时很难单独测试。有了声明式规则语言后你可以把规则文本和测试数据放到一起做表驱动测试String ruleText loadRuleFromFile(new-user-discount.rule); ListRule rules parser.parse(ruleText); RuleContext context new RuleContext(); context.setVariable(user, new User(u1, true, 0)); context.setVariable(order, new Order(o1, new BigDecimal(150))); engine.execute(rules, context); assertEquals(0, new BigDecimal(20).compareTo(order.getDiscount()));这种方法比在业务代码里模拟各种分支条件要清晰得多。6.5 一个容易被忽略的坑当规则文本中有多个规则时字符串切分和块解析的逻辑容易出错。建议在实际实现中对多个规则之间使用明确的空行分隔并在解析器中增加跳过空行的逻辑。本文的例子已经做了基本处理但如果你复制代码去扩展要注意when和then块之间、then和end之间不要漏掉换行。7. 最佳实践与工程建议到这里我们已经完成了一个可以运行的 Demo。但真实项目中要把一个声明式规则语言用好还需要注意很多工程细节。7.1 规则文本如何管理绝不要把规则文本直接硬编码在 Java 代码里。推荐的做法是把规则文本存储在配置文件中适合改动频率很低的规则。把规则文本存储在数据库中适合可以后台动态维护的规则。引入配置中心例如 Apollo、Nacos管理规则适合需要灰度发布和快速回滚的场景。规则的版本管理同样重要。每次修改规则都应有记录方便在出问题时回溯到上一个版本。7.2 规则引擎的性能优化反射是方便但性能一般。如果你在一个高并发接口里每次都解析规则、反射读取属性肯定会有性能问题。可以做以下优化规则预热系统启动时解析全部规则并缓存不要在请求时解析。反射缓存把字段的Field对象缓存起来避免每次获取。规则索引如果规则很多可以按用户级别、订单金额等维度建立简单索引减少匹配时的无效遍历。批量执行一次性传入多个对象批量执行规则减少上下文创建开销。7.3 规则的可观测性规则执行的过程必须留下日志至少记录以下信息当前请求的上下文 ID。命中了哪些规则。执行了哪些动作。规则执行前后关键业务字段的值变化。有了这些日志才能真正定位“为什么这个订单没有打折”这类问题。如果规则引擎执行结果异常也要提供规则回放的能力。7.4 安全与授权边界如果你的规则允许业务人员在线编辑就必须考虑安全边界规则解析器不能支持任意代码执行否则就是暴露了一个远程代码执行漏洞。对可用的字段、操作符、动作类型要有白名单限制。规则变更需要审批流程避免误操作影响线上业务。生产环境使用规则动态更新时必须先在测试环境验证。这些安全边界在 Drools、Easy Rules 等成熟框架中通常有对应的配置。自己实现时一定不能忽略。7.5 避免过度设计最后一条建议是不要为了用规则引擎而用规则引擎。如果你的业务规则很少、变化很慢、逻辑简单那 if-else 可能是更合适的选择。引入声明式规则语言会增加解析、调试、维护的成本。只有当规则数量多、变化频繁、需要业务人员参与维护时这套方案的收益才会显现。8. 总结与后续学习路线本文围绕 Lemma 这个“声明式业务规则语言”的概念展开从一个具体场景出发理解了声明式规则表达相较于命令式 if-else 的优势然后完整实现了一个最小的 Lemma 风格规则引擎。这个引擎虽然简单但已经包含了规则解析、条件匹配、动作执行的全链路。你可以顺着以下方向继续深入学习成熟的规则引擎例如Drools看它如何处理规则冲突、优先级、规则流等复杂问题。研究ANTLR用形式化的语法定义来表达更复杂的规则文法替代手写正则解析。结合Apollo 或 Nacos 配置中心把规则文本做成动态配置实现规则热更新。了解业务规则管理系统BRMS看一个完整的规则管理后台应该包含哪些功能例如版本控制、测试沙箱、审批流。规则引擎这条路知道的人很多真正动手实现过的人不多。如果你已经跟着本文把 Demo 跑通再去看 Drools 的文档时会轻松很多。如果这篇文章对你有帮助可以收藏备用。后续我会继续分享规则引擎在真实项目中的落地经验包括规则热更新、规则测试框架、规则和 DDD 领域模型的结合方式等欢迎关注交流。

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

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

免费获取报价