当前位置: 首页 > news >正文

从零实现一个可扩展的规则解析引擎 —— 支持 AND/OR 优先级、短路求值与多类型运算符

在日常业务开发中,我们经常需要基于一些“规则”来决定程序的走向。比如:

  • 客服机器人 根据用户问题领域和复杂度选择不同的模型;
  • 营销系统 根据用户画像匹配不同优惠券;
  • 风控引擎 根据请求参数、时间、分值判定是否放行。

这些规则往往以「条件 + 条件组 + 规则」的形式组织,支持 AND/OR 逻辑、字符串匹配、数值比较、日期比较、布尔运算等。
本文将手把手带大家实现一个可扩展的规则解析引擎,最终效果如下:

  • 支持 EQUALS / NOT_EQUALS / CONTAINS / IN / GREATER_THAN / LESS_THAN / REGEX 多种运算符;
  • 支持 STRING / NUMBER / DATE / BOOLEAN 多种数据类型;
  • 逻辑解析采用“AND 优先于 OR”的优先级;
  • 短路求值:在 AND 为 false 或 OR 为 true 时提前结束计算;
  • 代码可嵌入 Spring Boot,对接 REST API。

(完整项目代码附在文末,感兴趣的小伙伴可以自取hh~)


一、规则结构设计

我们将规则分为三层:

  1. 条件(Condition):原子判断,如「问题领域 == 编译原理」。
  2. 条件组(Group):由多个条件组成,内部支持 AND/OR。
  3. 规则(Rule):由多个条件组组成,组之间也支持 AND/OR。

在这里插入图片描述

前端传参示例(JSON):

{"rule": {"groups": [{"groupsOperator": "","conditions": [{ "field": "问题领域", "operator": "EQUALS", "value": "编译原理", "fieldType": "STRING", "groupOperator": "" },{ "field": "问题类别", "operator": "IN", "value": "静态分析,代码优化", "fieldType": "STRING", "groupOperator": "AND" },{ "field": "问题名称", "operator": "EQUALS", "value": "编译优化", "fieldType": "STRING", "groupOperator": "OR" }]},{"groupsOperator": "AND","conditions": [{ "field": "score", "operator": "GREATER_THAN", "value": "0.9", "fieldType": "NUMBER", "groupOperator": "" }]}]},"context": {"问题领域": "语法分析","问题类别": "LR","问题名称": "编译优化","score": 0.91}
}

对应逻辑表达式:

((问题领域 == 编译原理 AND 问题类别 ∈ {静态分析,代码优化}) OR 问题名称 == 编译优化)
AND (score > 0.9)

二、关键实现思路

1. 工具类(类型解析与容错)

  • 字符串转数字、日期、布尔值时采用 Optional,解析失败直接返回空。
  • IN 运算符支持逗号分隔。
  • REGEX 支持动态编译正则。

2. 条件求值器

负责判断单条条件是否成立:

  • STRING: equals/contains/in/regex
  • NUMBER: equals/greater/less/in
  • DATE: equals/greater/less/in
  • BOOLEAN: equals/not equals

3. 逻辑解析 —— AND 优先于 OR

这是本引擎的核心:

  • 错误做法:简单从左到右短路,会把 (A AND B) OR C 错判成 (A AND B) 不成立直接 false。

  • 正确做法先按 OR 分块,块内做 AND,再对块结果做 OR。

    • 条件层:cond[0] groupOperator cond[1] ...
    • 规则层:group[0] groupsOperator group[1] ...

算法逻辑:

// AND 优先级高于 OR : 按照 OR 切块,块内做 AND,块间做 OR
Boolean currentAnd = null;  // 当前块的累计结果
boolean anyOrTrue = false;  // 之前是否已有 AND 块为 True (用于整体 OR)for (元素 e : 序列) {if (第一个元素) {  // 第一个条件,开启 AND 块currentAnd = eval(e);continue;}if (连接符是AND) {  // AND:块内与currentAnd = currentAnd && eval(e);} else { // OR:结束上一个 AND 块,合入总结果anyOrTrue = anyOrTrue || Boolean.TRUE.equals(currentAnd);if (anyOrTrue) return true; // OR短路currentAnd = eval(e); // 开新 AND 块}
}
// 合并最后一块
anyOrTrue = anyOrTrue || Boolean.TRUE.equals(currentAnd);
return anyOrTrue;

三、代码示例

以条件组求值为例(AND > OR):

private boolean evalGroup(RuleDetailVO.GroupVO group, Map<String, Object> ctx) {List<RuleDetailVO.CondVO> conds = group.getConditions();if (conds == null || conds.isEmpty()) return false;Boolean currentAnd = null;boolean anyOrTrue = false;for (int i = 0; i < conds.size(); i++) {RuleDetailVO.CondVO c = conds.get(i);boolean curr = condEval.test(c, ctx);String join = c.getGroupOperator();boolean isOrJoin = RuleEvalUtils.isOr(join);if (i == 0) {currentAnd = curr;continue;}if (!isOrJoin) { // ANDcurrentAnd = currentAnd && curr;} else {         // ORanyOrTrue = anyOrTrue || Boolean.TRUE.equals(currentAnd);if (anyOrTrue) return true;currentAnd = curr;}}anyOrTrue = anyOrTrue || Boolean.TRUE.equals(currentAnd);return anyOrTrue;
}

规则层求值同理,只是把元素换成


四、测试用例

测试例1:

POST http://localhost:8080/api/rules/match

Content-Type:application/json

{"rule": {"groups": [{"groupsOperator": "","conditions": [{ "field": "问题领域", "operator": "EQUALS", "value": "编译原理", "fieldType": "STRING", "groupOperator": "" },{ "field": "问题类别", "operator": "IN", "value": "静态分析,代码优化", "fieldType": "STRING", "groupOperator": "AND" },{ "field": "问题名称", "operator": "EQUALS", "value": "编译优化", "fieldType": "STRING", "groupOperator": "OR" }]},{"groupsOperator": "AND","conditions": [{ "field": "score", "operator": "GREATER_THAN", "value": "0.9", "fieldType": "NUMBER", "groupOperator": "" }]}]},"context": {"问题领域": "语法分析","问题类别": "LR","问题名称": "编译优化","score": 0.91}
}

响应体:

{"hit": true
}

测试例2:

POST http://localhost:8080/api/rules/match

Content-Type:application/json

{"rule": {"groups": [{"groupsOperator": "","conditions": [{ "field": "问题领域", "operator": "EQUALS", "value": "编译原理", "fieldType": "STRING", "groupOperator": "" },{ "field": "问题类别", "operator": "IN", "value": "静态分析,代码优化", "fieldType": "STRING", "groupOperator": "AND" },{ "field": "问题名称", "operator": "EQUALS", "value": "编译优化", "fieldType": "STRING", "groupOperator": "OR" }]},{"groupsOperator": "AND","conditions": [{ "field": "score", "operator": "GREATER_THAN", "value": "0.9", "fieldType": "NUMBER", "groupOperator": "" }]}]},"context": {"问题领域": "语法分析","问题类别": "LR","问题名称": "编译优化","score": 0.91}
}

响应体:

{"hit": false
}

五、读者可进一步扩展

  1. 枚举化运算符

    • 避免拼写错误,支持自动校验。
  2. Bean Validation

    • 校验前端传参是否合法(如 fieldType 必须在 ENUM 内)。
  3. 规则预编译

    • 将规则提前编译为 Predicate<Map<String,Object>>,执行时直接调用,提升性能。
  4. 匹配路径审计

    • 记录每条条件的判断结果,用于调试与可视化。

六、总结

本文实现了一个 高可扩展的规则解析引擎,核心亮点:

  • 类型多样化:支持字符串、数字、日期、布尔;
  • 运算符丰富:equals、contains、in、regex、比较运算;
  • 逻辑正确性:严格保证 AND 优先于 OR,避免短路误判;
  • 短路优化:提升性能;
  • 可扩展性:支持与 Spring Boot 集成、缓存、审计等功能。

通过这种设计,我们可以在实际业务场景中灵活定义规则,既满足 前端可配置化,又保证 后端执行的确定性和高效性


附项目完整代码:

目录结构如下

rule-engine-springboot/
├─ pom.xml
└─ src/└─ main/├─ java/│  └─ per/│     └─ mjn/│        ├─ RuleEngineApplication.java│        ├─ common/│        │  └─ vo/│        │     └─ RuleDetailVO.java│        ├─ engine/│        │  ├─ ConditionEvaluator.java│        │  ├─ RuleEngine.java│        │  └─ RuleEvalUtils.java│        └─ web/│           ├─ MatchController.java│           └─ dto/│              ├─ MatchRequest.java│              └─ MatchResponse.java└─ resources/└─ application.yml

RuleDetailVO.java

package per.mjn.common.vo;import lombok.Data;import java.util.ArrayList;
import java.util.List;@Data
public class RuleDetailVO {private List<GroupVO> groups = new ArrayList<>();@Datapublic static class GroupVO {private String groupsOperator;private List<CondVO> conditions;}@Datapublic static class CondVO {private String field;private String operator;private String value;private String fieldType;private String groupOperator;}
}

ConditionEvaluator.java

package per.mjn.engine;import per.mjn.common.vo.RuleDetailVO;import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;public class ConditionEvaluator {public boolean test(RuleDetailVO.CondVO cond, Map<String, Object> ctx) {final String field = RuleEvalUtils.normField(cond.getField());final String op = cond.getOperator();final String fieldType = cond.getFieldType();final String expect = cond.getValue();final Object actual = ctx.get(field);if (!RuleEvalUtils.hasText(field) || !RuleEvalUtils.hasText(op)) return false;return switch (fieldType == null ? "STRING" : fieldType.toUpperCase()) {case "NUMBER" -> compareNumber(op, actual, expect);case "DATE" -> compareDate(op, actual, expect);case "BOOLEAN" -> compareBoolean(op, actual, expect);default -> compareString(op, actual, expect);};}private boolean compareString(String op, Object actual, String expect) {String a = RuleEvalUtils.asString(actual);if (a == null || expect == null) return false;return switch (op) {case "EQUALS" -> a.equals(expect);case "NOT_EQUALS" -> !a.equals(expect);case "CONTAINS" -> a.contains(expect);case "IN" -> RuleEvalUtils.parseInList(expect).contains(a);case "REGEX" -> Pattern.compile(expect).matcher(a).find();default -> false;};}private boolean compareNumber(String op, Object actual, String expect) {Optional<BigDecimal> aOpt = RuleEvalUtils.asDecimal(actual);Optional<BigDecimal> eOpt = RuleEvalUtils.asDecimal(expect);if (aOpt.isEmpty() || eOpt.isEmpty()) return false;BigDecimal a = aOpt.get();BigDecimal e = eOpt.get();int cmp = a.compareTo(e);return switch (op) {case "EQUALS" -> cmp == 0;case "NOT_EQUALS" -> cmp != 0;case "GREATER_THAN" -> cmp > 0;case "LESS_THAN" -> cmp < 0;case "IN" -> RuleEvalUtils.parseInList(expect).stream().anyMatch(s -> RuleEvalUtils.asDecimal(s).map(v -> v.compareTo(a) == 0).orElse(false));default -> false;};}private boolean compareBoolean(String op, Object actual, String expect) {Optional<Boolean> aOpt = RuleEvalUtils.asBoolean(actual);Optional<Boolean> eOpt = RuleEvalUtils.asBoolean(expect);if (aOpt.isEmpty() || eOpt.isEmpty()) return false;boolean a = aOpt.get();boolean e = eOpt.get();return switch (op) {case "EQUALS" -> a == e;case "NOT_EQUALS" -> a != e;default -> false;};}private boolean compareDate(String op, Object actual, String expect) {Optional<LocalDate> aOpt = RuleEvalUtils.asDate(actual);Optional<LocalDate> eOpt = RuleEvalUtils.asDate(expect);if (aOpt.isEmpty() || eOpt.isEmpty()) return false;LocalDate a = aOpt.get();LocalDate e = eOpt.get();int cmp = a.compareTo(e);return switch (op) {case "EQUALS" -> cmp == 0;case "NOT_EQUALS" -> cmp != 0;case "GREATER_THAN" -> cmp > 0;case "LESS_THAN" -> cmp < 0;case "IN" -> RuleEvalUtils.parseInList(expect).stream().anyMatch(s -> RuleEvalUtils.asDate(s).map(v -> v.compareTo(a) == 0).orElse(false));default -> false;};}
}

RuleEngine.java

package per.mjn.engine;import per.mjn.common.vo.RuleDetailVO;import java.util.List;
import java.util.Map;public class RuleEngine {private final ConditionEvaluator condEval = new ConditionEvaluator();public boolean matches(RuleDetailVO rule, Map<String, Object> ctx) {List<RuleDetailVO.GroupVO> groups = rule.getGroups();if (groups == null || groups.isEmpty()) return false;Boolean currentAnd = null;boolean anyOrTrue = false;for (int i = 0; i < groups.size(); i++) {RuleDetailVO.GroupVO g = groups.get(i);boolean curr = evalGroup(g, ctx);String join = g.getGroupsOperator();boolean isOrJoin = RuleEvalUtils.isOr(join);if (i == 0) {currentAnd = curr;continue;}if (!isOrJoin) {currentAnd = currentAnd && curr;} else {anyOrTrue = anyOrTrue || currentAnd;if (anyOrTrue) return true;currentAnd = curr;}}anyOrTrue = anyOrTrue || currentAnd;return anyOrTrue;}private boolean evalGroup(RuleDetailVO.GroupVO group, Map<String, Object> ctx) {List<RuleDetailVO.CondVO> conds = group.getConditions();if (conds == null || conds.isEmpty()) return false;// AND 优先级高于 OR : 按照 OR 切块,块内做 AND,块间做 ORBoolean currentAnd = null;  // 当前块的累计结果boolean anyOrTrue = false;  // 之前是否已有 AND 块为 True (用于整体 OR)for (int i = 0; i < conds.size(); i++) {RuleDetailVO.CondVO c = conds.get(i);boolean curr = condEval.test(c, ctx);String join = c.getGroupOperator();  // 当前条件与“上一条”的连接符boolean isOrJoin = RuleEvalUtils.isOr(join);if (i == 0) {  // 第一个条件,开启 AND 块currentAnd = curr;continue;}if (!isOrJoin) {  // AND 块内与currentAnd = currentAnd && curr;} else {  // OR:结束上一个 AND 块,合入总结果anyOrTrue = anyOrTrue || currentAnd;if (anyOrTrue) return true; // OR 短路currentAnd = curr;  // 开新 AND 块}}// 合并最后一个 AND 块anyOrTrue = anyOrTrue || currentAnd;return anyOrTrue;}
}

RuleEvalUtils.java

package per.mjn.engine;import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;public final class RuleEvalUtils {private RuleEvalUtils() {}public static boolean hasText(String s) {return s != null && !s.isBlank();}public static boolean isOr(String op) {return "OR".equalsIgnoreCase(op);}public static String normField(String f) {return f == null ? "" : f.trim();}public static List<String> parseInList(String v) {if (v == null) return List.of();String[] arr = v.split(",");List<String> list = new ArrayList<>(arr.length);for (String s : arr) {if (!s.isBlank()) list.add(s.trim());}return list;}public static String asString(Object o) {return (o == null) ? null : String.valueOf(o);}public static Optional<BigDecimal> asDecimal(Object o) {try {if (o == null) return Optional.empty();if (o instanceof BigDecimal bd) return Optional.of(bd);if (o instanceof Number n) return Optional.of(new BigDecimal(n.toString()));if (o instanceof String s && !s.isBlank()) return Optional.of(new BigDecimal(s.trim()));} catch (Exception ignore) {}return Optional.empty();}public static Optional<Boolean> asBoolean(Object o) {if (o == null) return Optional.empty();if (o instanceof Boolean b) return Optional.of(b);if (o instanceof String s) {String x = s.trim().toLowerCase(Locale.ROOT);if ("true".equals(x) || "1".equals(x) || "yes".equals(x)) return Optional.of(true);if ("false".equals(x) || "0".equals(x) || "no".equals(x)) return Optional.of(false);}return Optional.empty();}public static Optional<LocalDate> asDate(Object o) {if (o == null) return Optional.empty();try {if (o instanceof LocalDate d) return Optional.of(d);if (o instanceof OffsetDateTime odt) return Optional.of(odt.toLocalDate());if (o instanceof String s && !s.isBlank()) {DateTimeFormatter f = DateTimeFormatter.ISO_LOCAL_DATE;return Optional.of(LocalDate.parse(s.trim(), f));}} catch (Exception ignore) {}return Optional.empty();}
}

MatchRequest.java

package per.mjn.web.dto;import jakarta.validation.constraints.NotNull;
import lombok.Data;
import per.mjn.common.vo.RuleDetailVO;
import java.util.Map;@Data
public class MatchRequest {@NotNullprivate RuleDetailVO rule;@NotNullprivate Map<String, Object> context;
}

MatchResponse.java

package per.mjn.web.dto;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class MatchResponse {private boolean hit;
}

MatchController.java

package per.mjn.web;import jakarta.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import per.mjn.engine.RuleEngine;
import per.mjn.web.dto.MatchRequest;
import per.mjn.web.dto.MatchResponse;@RestController
@RequestMapping(path = "/api/rules", produces = MediaType.APPLICATION_JSON_VALUE)
public class MatchController {private final RuleEngine ruleEngine;public MatchController() {this.ruleEngine = new RuleEngine();}@PostMapping(path = "/match", consumes = MediaType.APPLICATION_JSON_VALUE)public MatchResponse match(@RequestBody @Valid MatchRequest req) {boolean hit = ruleEngine.matches(req.getRule(), req.getContext());return new MatchResponse(hit);}
}

RuleEngineApplication.java

package per.mjn;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class RuleEngineApplication {public static void main(String[] args) {SpringApplication.run(RuleEngineApplication.class, args);}}

application.yml

server:port: 8080
spring:jackson:serialization:WRITE_DATES_AS_TIMESTAMPS: false

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.2</version><relativePath/></parent><groupId>per.mjn</groupId><artifactId>rule-engine-springboot</artifactId><version>0.0.1-SNAPSHOT</version><name>rule-engine-springboot</name><description>rule-engine-springboot</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
http://www.dtcms.com/a/358332.html

相关文章:

  • Vue2之axios在脚手架中的使用以及前后端交互
  • RabbitMQ 和 Kafka
  • 函数(2)
  • 并发编程——08 Semaphore源码分析
  • 免费在线图片合成视频工具 ,完全免费
  • 文件夹命名软件,批量操作超简单
  • 美团8-30:编程题
  • 深入解析前缀和算法:原理、实现与应用
  • 医疗AI时代的生物医学Go编程:高性能计算与精准医疗的案例分析(六)
  • react组件
  • C++优先级队列priority_queue的模拟实现
  • Trailing Zeros (计算 1 ~ n 中质因子 p 的数量)
  • Java全栈开发面试实战:从基础到高并发的全面解析
  • Redis数据类型概览:除了五大基础类型还有哪些?
  • leetcode643. 子数组最大平均数 I
  • AI-调查研究-65-机器人 机械臂控制技术的前世今生:从PLC到MPC
  • vscode+cmake+mingw64+opencv环境配置
  • wpf之依赖属性
  • 具有类人先验知识的 Affordance-觉察机器人灵巧抓取
  • C++_多态和虚构
  • 卡片一放,服务直达!实现信息零层级触达
  • Python实现京东商品数据自动化采集的实用指南
  • (双指针)Leetcode283.移动零-替换数字类别+Leetcode15. 三数之和
  • UI前端大数据可视化实战策略:如何设计符合用户认知的数据可视化界面?
  • 【计算机网络】HTTP是什么?
  • Ansible Playbook 调试与预演指南:从语法检查到连通性排查
  • 一体化步进伺服电机在汽车线束焊接设备中的应用案例
  • MongoDB 源码编译与调试:深入理解存储引擎设计 内容详细
  • HarmonyOS元服务开发
  • 深入解析HarmonyOS:UIAbility与Page的生命周期协同