背景近期因新项目需求，可能需要引入规则引擎，因此花费了一些时间对相关技术进行了调研。通过主流搜索引擎检索“规则引擎”关键字，可以找到大量相关资料，其中绝大部分都会提到 Drools、URules、Easy-Rules 等开源项目，部分文章也指出它们采用 Groovy 脚本来实现。通过对项目需求的评估，初步判定 Groovy 脚本方案已能满足实际场景。然而，在这些资料或方案中，除 URules 外，大部分仅关注框架的性能和使用便捷性，很少探讨如何让业务人员自行定义规则的方案。URules 虽然自带可视化规则管理界面，但其界面样式难以自定义，无法与现有的后台管理界面无缝融合。经过多次尝试变换关键字检索，最终在 StackOverflow 上找到了一个探讨该问题的帖子。特此将帖子中提到的方案分享给大家，如果你也在研究同样的问题，希望本文能对你有所帮助。在介绍具体方案之前，我们先简单了解一下什么是规则引擎。什么是规则引擎？简单来说，规则引擎负责的核心逻辑是：判定某个数据或对象是否满足特定条件，并根据判定结果执行相应的动作。例如：对于刚刚在网站上完成购物的一个用户（对象）， 如果她是 "女性用户 并且（连续登录天数大于 10 天 或者 订单金额大于 200 元）" (条件.html)， 那么系统就自动给该用户发放一张优惠券（动作）。在上述场景中，规则引擎最重要的优势在于实现了“条件”表达式的配置化。如果条件表达式无法配置，程序员就需要在代码中硬编码各种 if...else... 逻辑；若条件组合特别复杂，代码将难以维护。同时，若无法配置化，每次条件的细微变更都需要修改代码并走发布流程，无法快速响应业务需求。在 Groovy 脚本方案中，上述场景可以这样实现：定义一个 Groovy 脚本：def validateCondition(args.html) { return args.用户性别 == "女性" && (args.连续登录天数 > 10 || args.订单金额 > 200.html); }通过 Java 提供的 ScriptEngineManager 对象去执行： org.codehaus.groovy groovy 3.0.7 /* * @param condition 从数据库中读出来的条件表达式 */ private Boolean validateCondition(String condition.html) { // 实际使用上，ScriptEngineManager 建议定义为单例 ScriptEngineManager engineManager = new ScriptEngineManager(); ScriptEngine engine = engineManager.getEngineByName("groovy".html); Map<String, Object> args = new HashMap<>(); args.put("用户性别", "女性".html); args.put("连续登录天数", 11.html); args.put("订单金额", 220.html); engine.eval(condition.html); return ((Invocable.html) engine).invokeFunction("validateCondition", args.html); }在上面的 Groovy 脚本中，经常需要变动的部分是 args.用户性别 == "女性" && (args.连续登录天数 > 10 || args.订单金额 > 200.html) 这个表达式。最简单的方案是在后台界面提供一个文本框，录入整个 Groovy 脚本后保存到数据库。但这种方案存在缺点：表达式的定义有一定门槛。这对程序员来说很简单，但对于未接触过编程的业务人员而言，很容易录入错误的表达式。这就引出了本文的另一个话题：如何实现 Boolean 表达式的可视化编辑？如何实现 Boolean 表达式的可视化编辑？一种方案是前端对指定的表达式进行语法解析，然后渲染成界面元素；业务人员编辑完成后，再将界面元素结构转换回表达式。然而，直接解析语法存在两个缺点：需要考虑的边界条件较多，容易因处理不当导致解析出错。限定了后端可选用的脚本语言。例如，上述方案选用的是 Groovy，其“与”运算符是 &&；假如某天引入了一种性能更好的脚本语言，其“与”运算符定义为 and，那么就需要修改大量表达式解析逻辑。另一种方案，是定义一个数据结构来描述表达式的结构：{ "all": [ { "any": [ { "gt": ["连续登录天数", 10] }, { "gt": ["订单金额", 200] } ] }, { "eq": ["用户性别", "女性"] } ] }然后，使用递归的方式解析该结构。对于前端开发，可以在递归解析过程中渲染成对应的界面元素；对于后端人员，则可以生成对应的 Boolean 表达式。有了 Boolean 表达式，就可以使用预定的脚本模板生成最终的规则。// 模板示例 def validateCondition(args.html) { return $s; }/** * 动态 Boolean 表达式解析器 */ public class RuleParser { private static final Map<String, String> operatorMap = new HashMap<>(); private static final ObjectMapper objectMapper = new ObjectMapper(); static { operatorMap.put("all", "&&".html); operatorMap.put("any", "||".html); operatorMap.put("ge", ">=".html); operatorMap.put("gt", ">".html); operatorMap.put("eq", "==".html); operatorMap.put("ne", "!=".html); operatorMap.put("le", "<=".html); operatorMap.put("lt", "<".html); } /** * 解析规则字符串，转换成表达式形式 * 示例: * 输入： * { "any": [ * { "all": [ * { "ge": ["A", 10] }, * { "eq": ["B", 20] } * ]}, * { "lt": ["C", 30] }, * { "ne": ["D", 50] } * ]} * * 输出： * ( A >= 10 && B == 20 .html) || ( C < 30 .html) || ( D != 50 .html) * * @param rule 规则的 JSON 字符串形式 * @return 返回 Boolean 表达式 * @throws IOException 解析 JSON 字符串异常 */ public static String parse(String rule.html) throws IOException { JsonNode jsonNode = objectMapper.readTree(rule.html); return parse(jsonNode.html); } /** * 解析规则节点，转换成表达式形式 * @param node Jackson Node * @return 返回 Boolean 表达式 */ private static String parse(JsonNode node.html) { // TODO: 支持变量的"arg."前缀定义 if (node.isObject(.html)) { Iterator<Map.Entry<String, JsonNode>> it = node.fields(); if (it.hasNext(.html)) { Map.Entry<String, JsonNode> entry = it.next(); List<String> arrayList = new ArrayList<>(); for (JsonNode jsonNode : entry.getValue(.html)) { arrayList.add(parse(jsonNode.html)); } return "(" + String.join(" " + operatorMap.get(entry.getKey(.html)) + " ", arrayList) + ")"; } else { // 兼容空节点：例如 {"all": [{}, "eq":{"A","1"}]} return " 1==1"; } } else if (node.isValueNode(.html)) { return node.asText(); } return ""; } }结语以上就是本文要阐述的全部内容。对于这个话题，如果你有这方面的经验或者更好的方案，也请多多指教，谢谢！完整源码：https://github.com/RongieZeng/rule-parser说明：文中代码示例基于 Groovy 3.0.7 及 Java 标准脚本引擎接口编写，实际使用时请确保引入 groovy-jsr223 等相关依赖以保证兼容性。