news 2026/8/28 14:21:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 if-else 到声明式规则引擎:手写一个 Lemma 风格 DSL

在公司里做业务需求的时候,不知道你有没有遇到过这种场景:产品经理提了一条规则,例如“新用户首单满 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 用 fastjson 做 JSON 解析,非必须,实际可按需引入 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.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 比较符与逻辑组合

有了基本结构,下一步要支持哪些比较符?

  • ==:等于
  • !=:不等于
  • >:大于
  • >=:大于等于
  • <:小于
  • <=:小于等于

这些比较符覆盖了绝大多数业务场景。

在表达更复杂的规则时,可能还需要andor逻辑组合。在最小实现中,我们可以约定:同一规则中的多个条件之间,默认是“并且”的关系。这样可以用最简单的数据结构表达。

如果一个规则需要“或者”关系,可以拆分成两条规则,或者后续扩展条件类型。在最小 Demo 阶段,保持简单更重要。

3.3 规则解析器的职责

规则解析器负责把一段字符串形式的规则文本,转换为内存中的Rule对象。解析器是声明式规则语言的关键部分,因为业务人员写的是文本,引擎执行的是对象。

一个比较清晰的规则文本格式,可以参考下面这个例子:

rule "新人满减" when user.new == true order.amount >= 100 then order.discount = 20 end

解析器读取这段文本后,需要:

  1. 识别规则名。
  2. 提取 when 块中的条件行。
  3. 提取 then 块中的动作行。
  4. 分别封装成RuleConditionRuleAction列表。

3.4 引擎的执行流程

引擎的执行流程比较固定,可以拆成三步:

  1. 加载规则:把规则文本解析成Rule对象列表。
  2. 匹配规则:针对传入的规则上下文(包含订单、用户等业务对象),逐条判断条件是否成立。
  3. 执行动作:对匹配成功的规则,执行动作列表中的赋值操作。

打个比方,这就像是在做一次过滤:规则库里有几十条规则,引擎拿到一个具体业务场景后,筛选出其中所有“当前场景适用”的规则,然后逐一执行它们的结果。

理解了这些核心概念后,下面就可以动手写代码了。

4. 完整实战:实现一个最小的 Lemma 风格规则引擎

这一节我们来实现一个可运行的 Demo。虽然代码量不大,但已经能够完整体现“声明式规则语言”从文本到执行的完整链路。

4.1 定义业务模型

我们先创建两个业务模型类:用户和订单。

文件路径:src/main/java/com/example/lemma/model/User.java

package 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.java

package 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.java

package 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.java

package 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.java

package com.example.lemma.rules; import java.util.ArrayList; import java.util.List; public class Rule { private String name; private List<RuleCondition> conditions = new ArrayList<>(); private List<RuleAction> actions = new ArrayList<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public List<RuleCondition> getConditions() { return conditions; } public void setConditions(List<RuleCondition> conditions) { this.conditions = conditions; } public List<RuleAction> getActions() { return actions; } public void setActions(List<RuleAction> actions) { this.actions = actions; } }

有了这几个类之后,解析器就可以开工了。

文件路径:src/main/java/com/example/lemma/rules/RuleParser.java

package 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 List<Rule> parse(String ruleText) { List<Rule> 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); } }

这个解析器做了一些简化处理:

  • 用正则表达式匹配字段 操作符 值格式。
  • whenthen关键字切分文本块。
  • 条件行和动作行均按换行符切分。

在实际项目中,你可能需要更鲁棒的解析方案,比如使用 ANTLR 定义词法和语法,但作为教学示例,这个解析器已经足够展示核心思路。

4.3 实现规则上下文与引擎

解析器把文本转换为Rule对象之后,引擎需要能够读取上下文中对象的具体属性值。这里我们借助反射来实现属性的读取和赋值。

文件路径:src/main/java/com/example/lemma/engine/RuleContext.java

package com.example.lemma.engine; import java.util.HashMap; import java.util.Map; public class RuleContext { private Map<String, 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 存放变量引用。userorder这样的对象会注册进去。

文件路径:src/main/java/com/example/lemma/engine/RuleEngine.java

package 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(List<Rule> 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); } } }

引擎的核心逻辑集中在matchesConditionapply两个方法中:

  • matchesCondition:根据操作符类型,对布尔、数字、字符串分别做比较。
  • apply:通过反射把规则中声明的值赋给目标字段。

要注意,BigDecimal的比较不能用equals,因为它是按精度比较的,1.01.00equals结果是false。这里用compareTo更合理。

4.4 编写启动类与测试接口

把以上组件串起来之后,我们写一个启动类和一个测试接口。

文件路径:src/main/java/com/example/lemma/LemmaDemoApplication.java

package 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.java

package 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 Map<String, Object> execute(@RequestBody Map<String, 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); // 解析并执行规则 List<Rule> 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 常见问题汇总

问题现象常见原因解决思路
规则解析失败规则文本中whenthen缺少换行检查规则文本格式,确认关键字后面跟的是换行符
条件永远不匹配属性名拼写错误,或反射读取不到属性检查字段中的变量名和属性名是否一致
比较结果不对数字类型混用,浮点数用equals比较优先用BigDecimal.compareTo比较数字
动作赋值不生效字段名写错,或类型转换失败检查动作行中的字段是否存在,类型是否受支持
多条规则互相覆盖规则执行顺序混乱设计优先级或冲突处理策略
规则数量多了以后性能差每条规则都走反射遍历缓存反射结果,引入索引或先粗糙筛选候选规则

6.2 属性读取不到怎么办

如果你执行规则时遇到无法读取属性异常,可以按以下步骤排查:

  1. 确认规则文本中的变量名是userorder这样的注册名。
  2. 确认变量名后的属性名是 Java 类中真实存在的字段名。
  3. 检查该字段是否是private,引擎中已经调用了setAccessible(true),如果不是这个原因,检查字段是否被static修饰。
  4. 确认字段名没有拼写错误,比如newUser不要写成newuser

6.3 规则优先级不明确

最小实现是“按规则集合的顺序依次执行”,这在实际项目中可能不够用。通常的解决方案有两种:

  • Rule增加一个priority字段,在执行前按优先级排序。
  • 匹配成功后立即中断,只执行第一条匹配的规则。

具体选择哪种方案,取决于业务需求。比如营销场景通常需要优惠叠加,就应该全部执行;而风控场景通常只摘取最高优先级的结果,就应该中断。

6.4 如何保证规则的可测试性

规则代码分散在 If-Else 中时,很难单独测试。有了声明式规则语言后,你可以把规则文本和测试数据放到一起,做表驱动测试:

String ruleText = loadRuleFromFile("new-user-discount.rule"); List<Rule> 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 一个容易被忽略的坑

当规则文本中有多个规则时,字符串切分和块解析的逻辑容易出错。建议在实际实现中,对多个规则之间使用明确的空行分隔,并在解析器中增加跳过空行的逻辑。本文的例子已经做了基本处理,但如果你复制代码去扩展,要注意whenthen块之间、thenend之间不要漏掉换行。

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 领域模型的结合方式等,欢迎关注交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/28 14:20:45

桌面麒麟系统添加字体

中标麒麟添加字体办法如下&#xff1a;1.从互联网上搜索需要的字体&#xff0c;格式后缀最好是.ttf格式的,或者从Windows系统c:\windows\Fonts 整个Fonts文件夹打包拷贝到/root/路径下。2.最好刻录到光盘导入到中标麒麟系统内&#xff08;搭建本地FTP传输也行&#xff09;。3.找…

作者头像 李华
网站建设 2026/8/28 14:20:40

Agent形态多变,AI Infra应围绕执行生命周期而建

Agent 形态一天一个样&#xff0c;Infra 到底该为谁而建&#xff1f; 如果你最近在写 Agent 相关项目&#xff0c;大概率会有一种感觉&#xff1a;今天刚把单 Agent 的问答流程跑通&#xff0c;明天社区就在讲多 Agent 协作编排&#xff1b;你还在纠结要不要引入某个 Agent 框…

作者头像 李华
网站建设 2026/8/28 14:19:33

VersaLogic Android评估套件解析:从AOSP到工业嵌入式实战

昨天在展会上路过VersaLogic的展台&#xff0c;看到他们推出了一款新的Android Demo/Eval Kit&#xff0c;旁边立了块“Enter to win”的抽奖牌。作为常年和嵌入式Linux打交道的老玩家&#xff0c;我一开始觉得这就是个营销噱头&#xff0c;但仔细了解之后发现&#xff0c;这个…

作者头像 李华
网站建设 2026/8/28 14:15:39

Python模块化设计实战:构建可维护的多级菜单系统

1. 项目概述&#xff1a;从零构建一个可维护的多级菜单系统 最近在带几个刚入门Python的朋友做小项目&#xff0c;发现他们写的代码里&#xff0c;菜单功能总是最头疼的部分。要么是满屏的 if-elif-else 嵌套&#xff0c;逻辑像一团乱麻&#xff0c;改个选项就得翻半天&#…

作者头像 李华