通用的规则匹配算法(原创)(java+.net)
1.java里可以使用Spring的 Spel或者Google的Aviator
如果使用 Aviator 则添加以下依赖
<dependency> <groupId>com.googlecode.aviator</groupId> <artifactId>aviator</artifactId> <version>4.1.2</version> </dependency>
不过,推荐使用Spel
一般的规则匹配最终都会采用如下表达式来计算
如 ( {status} in "2,3" && ({level} in "p1,p2" || {times} in "1,9"))
但是存储在DB中一般采用 List<Model>的方式来存储,这样方便管理界面的前端的渲染 (当然也不排除直接存储表达式的,不过前端的渲染就有些难度了)
整个解析过程实现过程有以下几步
1.存储的List中的规则转换为表达式
1.1 增加括号
1.2 替换变量
1.3 构造spel表达式
1.4 连接下一个规则
2.计算表达式
代码如下:
import com.google.common.collect.ImmutableMap;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor @AllArgsConstructor @Data static class RuleItem { /** * 左变量 */ private String left; /** * 比较表达式 */ private ComparelOpration comparelOpration; /** * 右变量或者常量 */ private String right; /** * 连接下一个表达式的逻辑运算符 */ private LogicalOpration logicalOpra; } @NoArgsConstructor @AllArgsConstructor @Data static class RuleModel { /** * 规则列表 */ private List<RuleItem> ruleItems; /** * 左括号放在第几个Item之前 */ private List<Integer> leftParenthesesIndex; /** * 右括号放在第几个Item之后 */ private List<Integer> rightParenthesesIndex; } @Data @AllArgsConstructor @NoArgsConstructor static class SpelResult { private String express; private StandardEvaluationContext context; }
使用的两个连接器(比较连接和逻辑连接)
enum ComparelOpration { In, NotIn, GreaterThan, LessThan, GreaterEqualThan, LessEqualThan, Equal, NotEqual; public static boolean isDecimalCompareLogicalOpration(ComparelOpration opration) { return opration.ordinal() == ComparelOpration.GreaterThan.ordinal() || opration.ordinal() == ComparelOpration.GreaterEqualThan.ordinal() || opration.ordinal() == ComparelOpration.LessEqualThan.ordinal() || opration.ordinal() == ComparelOpration.LessThan.ordinal(); } public static boolean isEqualLogicalOpration(ComparelOpration opration) { return opration.ordinal() == ComparelOpration.Equal.ordinal() || opration.ordinal() == ComparelOpration.NotEqual.ordinal() ; } } enum LogicalOpration { None, And, Or; static String toStr(LogicalOpration logicalOpration) { return logicalOpration.ordinal() == LogicalOpration.None.ordinal() ? "" : (logicalOpration.ordinal() == LogicalOpration.And.ordinal() ? "&&" : "||"); } }
匹配工厂如下
static class SpelMatchFactory { private static final ExpressionParser parser = new SpelExpressionParser(); static SpelResult toSpelExpress(RuleModel model, Map<String, String> userFeature) { List<RuleItem> ruleItemList = model.getRuleItems(); StringBuilder sb = new StringBuilder(); StandardEvaluationContext ctx = new StandardEvaluationContext(); for (int i = 0; i < ruleItemList.size(); i++) { RuleItem item = ruleItemList.get(i); if (model.leftParenthesesIndex.contains(i)) { sb.append("("); } String listKey = "list" + i; String valueKey = "item" + i; String subExpress = compute(item, listKey, valueKey); sb.append(subExpress); String leftValue = item.getLeft(); if (leftValue.startsWith("{") && leftValue.endsWith("}")) { leftValue = userFeature.get(leftValue.substring(1, leftValue.length() - 1)); } String rightValue = item.getRight(); if (rightValue.startsWith("{") && rightValue.endsWith("}")) { rightValue = userFeature.get(rightValue.substring(1, rightValue.length() - 1)); } // 这里暂时只支持 Integer和String 两种变量类型 if (ComparelOpration.isDecimalCompareLogicalOpration(item.comparelOpration)) { ctx.setVariable(listKey, Integer.parseInt(rightValue)); ctx.setVariable(valueKey, Integer.parseInt(leftValue)); } else if (ComparelOpration.isEqualLogicalOpration(item.comparelOpration)) { ctx.setVariable(listKey, rightValue); ctx.setVariable(valueKey, leftValue); } else { ctx.setVariable(listKey, Arrays.asList(rightValue.split(","))); ctx.setVariable(valueKey, leftValue); } if (model.rightParenthesesIndex.contains(i)) { sb.append(")"); } if (item.logicalOpra.ordinal() != LogicalOpration.None.ordinal()) { sb.append(LogicalOpration.toStr(item.getLogicalOpra())); } } return new SpelResult(sb.toString(), ctx); } public static boolean compute(RuleModel model, Map<String, String> userFeature) { SpelResult spelExpressResult = SpelMatchFactory.toSpelExpress(model, userFeature); Boolean execResult = parser.parseExpression(spelExpressResult.getExpress()).getValue( spelExpressResult.getContext(), Boolean.class); return execResult; } private static String compute(RuleItem matchItem, String listKey, String valueKey) { if (matchItem.getComparelOpration().ordinal() == ComparelOpration.Equal.ordinal()) { return "#" + listKey + ".equals(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.NotEqual.ordinal()) { return "!#" + listKey + ".equals(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.In.ordinal()) { return "#" + listKey + ".contains(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.NotIn.ordinal()) { return "!#" + listKey + ".contains(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.GreaterEqualThan.ordinal()) { return "#" + valueKey + ">=" + "#" + listKey; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.LessEqualThan.ordinal()) { return "#" + valueKey + "<=" + "#" + listKey; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.GreaterThan.ordinal()) { return "#" + valueKey + ">" + "#" + listKey; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.LessThan.ordinal()) { return "#" + valueKey + "<" + "#" + listKey; } throw new IllegalArgumentException("不支持的逻辑运算类型"); } }
最后 ,测试代码如下:
public static void main(String[] args) { List<RuleItem> ruleItems = new ArrayList<>(); ruleItems.add(new RuleItem("{status}", ComparelOpration.In, "2,3", LogicalOpration.Or)); ruleItems.add(new RuleItem("{level}", ComparelOpration.In, "1,2", LogicalOpration.And)); ruleItems.add(new RuleItem("{hours}", ComparelOpration.GreaterEqualThan, "48", LogicalOpration.And)); ruleItems.add(new RuleItem("{phone1}", ComparelOpration.Equal, "{phone2}", LogicalOpration.None)); RuleModel model = new RuleModel(); model.setRuleItems(ruleItems); //左括号在0的位置之前 model.setLeftParenthesesIndex(Arrays.asList(0)); //右括号在1的位置之后 model.setRightParenthesesIndex(Arrays.asList(1)); //以上表达式相当于 ({status} in '2,3' or {level} in '1,2') && {hours}>=48 && {phone1}=={phone2} //1. {phone1} != {phone2} ,结果为false Map<String, String> userFeature1 = ImmutableMap.of("status", "2", "level", "1", "phone1", "13900000000", "phone2", "13900000001", "hours", "66"); boolean computeResult = SpelMatchFactory.compute(model, userFeature1); System.out.println("userFeature1的匹配结果:" + computeResult); //2.{hours} < 48 ,结果为false Map<String, String> userFeature2 = ImmutableMap.of("status", "2", "level", "1", "phone1", "13900000000", "phone2", "13900000001", "hours", "6"); computeResult = SpelMatchFactory.compute(model, userFeature2); System.out.println("userFeature2的匹配结果:" + computeResult); //3. {status} 不在 2,3 中,但是 level 在 1,2中,结果为true Map<String, String> userFeature3 = ImmutableMap.of("status", "1", "level", "1", "phone1", "13900000000", "phone2", "13900000000", "hours", "66"); computeResult = SpelMatchFactory.compute(model, userFeature3); System.out.println("userFeature3的匹配结果:" + computeResult); //4. {status} 不在 2,3 中,且 level 不在 1,2中,结果为false Map<String, String> userFeature4 = ImmutableMap.of("status", "1", "level", "3", "phone1", "13900000000", "phone2", "13900000000", "hours", "66"); computeResult = SpelMatchFactory.compute(model, userFeature4); System.out.println("userFeature4的匹配结果:" + computeResult); //4.一切都匹配,返回true Map<String, String> userFeature5 = ImmutableMap.of("status", "2", "level", "1", "phone1", "13900000000", "phone2", "13900000000", "hours", "66"); computeResult = SpelMatchFactory.compute(model, userFeature5); System.out.println("userFeature5的匹配结果:" + computeResult); }
输出结果为:
表达式:(#list0.contains(#item0)||#list1.contains(#item1))&&#item2>=#list2&&#list3.equals(#item3) userFeature1的匹配结果:false 表达式:(#list0.contains(#item0)||#list1.contains(#item1))&&#item2>=#list2&&#list3.equals(#item3) userFeature2的匹配结果:false 表达式:(#list0.contains(#item0)||#list1.contains(#item1))&&#item2>=#list2&&#list3.equals(#item3) userFeature3的匹配结果:true 表达式:(#list0.contains(#item0)||#list1.contains(#item1))&&#item2>=#list2&&#list3.equals(#item3) userFeature4的匹配结果:false 表达式:(#list0.contains(#item0)||#list1.contains(#item1))&&#item2>=#list2&&#list3.equals(#item3) userFeature5的匹配结果:true
完整代码如下
import com.google.common.collect.ImmutableMap; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; import java.math.BigDecimal; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class SpelMatchFactory { private static final ExpressionParser parser = new SpelExpressionParser(); static SpelResult toSpelExpress(RuleModel model, Map<String, String> userFeature) { List<RuleItem> ruleItemList = model.getRuleItems(); StringBuilder sb = new StringBuilder(); StandardEvaluationContext ctx = new StandardEvaluationContext(); for (int i = 0; i < ruleItemList.size(); i++) { RuleItem item = ruleItemList.get(i); if (model.leftParenthesesIndex.contains(i)) { sb.append("("); } String listKey = "list" + i; String valueKey = "item" + i; String subExpress = compute(item, listKey, valueKey); sb.append(subExpress); String leftValue = item.getLeft(); if (leftValue.startsWith("{") && leftValue.endsWith("}")) { leftValue = userFeature.get(leftValue.substring(1, leftValue.length() - 1)); } String rightValue = item.getRight(); if (rightValue.startsWith("{") && rightValue.endsWith("}")) { rightValue = userFeature.get(rightValue.substring(1, rightValue.length() - 1)); } if (ComparelOpration.isDecimalCompareLogicalOpration(item.comparelOpration)) { ctx.setVariable(listKey, new BigDecimal(rightValue)); ctx.setVariable(valueKey, new BigDecimal(leftValue)); } else if (ComparelOpration.isEqualLogicalOpration(item.comparelOpration)) { ctx.setVariable(listKey, rightValue); ctx.setVariable(valueKey, leftValue); } else { if (rightValue.startsWith("'") && rightValue.endsWith("'")) { rightValue = rightValue.substring(1, rightValue.length() - 1); } if (rightValue.startsWith("\"") && rightValue.endsWith("\"")) { rightValue = rightValue.substring(1, rightValue.length() - 1); } ctx.setVariable(listKey, Arrays.asList(rightValue.split(","))); ctx.setVariable(valueKey, leftValue); } if (model.rightParenthesesIndex.contains(i)) { sb.append(")"); } if (item.logicalOpra.ordinal() != LogicalOpration.None.ordinal()) { sb.append(LogicalOpration.toStr(item.getLogicalOpra())); } } return new SpelResult(sb.toString(), ctx); } public static boolean compute(String expression, Map<String, String> userFeature) { RuleModel model = toRuleModel(expression); return compute(model, userFeature); } public static boolean compute(RuleModel model, Map<String, String> userFeature) { SpelResult spelExpressResult = SpelMatchFactory.toSpelExpress(model, userFeature); Boolean execResult = parser .parseExpression(spelExpressResult.getExpress()) .getValue(spelExpressResult.getContext(), Boolean.class); return execResult; } private static String compute(RuleItem matchItem, String listKey, String valueKey) { if (matchItem.getComparelOpration().ordinal() == ComparelOpration.Equal.ordinal()) { return "#" + listKey + ".equals(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.NotEqual.ordinal()) { return "!#" + listKey + ".equals(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.In.ordinal()) { return "#" + listKey + ".contains(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.NotIn.ordinal()) { return "!#" + listKey + ".contains(#" + valueKey + ")"; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.GreaterEqualThan.ordinal()) { return "#" + valueKey + ">=" + "#" + listKey; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.LessEqualThan.ordinal()) { return "#" + valueKey + "<=" + "#" + listKey; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.GreaterThan.ordinal()) { return "#" + valueKey + ">" + "#" + listKey; } if (matchItem.getComparelOpration().ordinal() == ComparelOpration.LessThan.ordinal()) { return "#" + valueKey + "<" + "#" + listKey; } throw new IllegalArgumentException("不支持的逻辑运算类型"); } @NoArgsConstructor @AllArgsConstructor @Data static class RuleItem { /** * 左变量 */ private String left; /** * 比较表达式 */ private ComparelOpration comparelOpration; /** * 右变量或者常量 */ private String right; /** * 连接下一个表达式的逻辑运算符 */ private LogicalOpration logicalOpra = LogicalOpration.None; @Override public String toString() { return String.join(" ", left, comparelOpration.getCode(), right); } } @NoArgsConstructor @AllArgsConstructor @Data static class RuleModel { /** * 规则列表 */ private List<RuleItem> ruleItems; /** * 左括号放在第几个Item之前 */ private List<Integer> leftParenthesesIndex; /** * 右括号放在第几个Item之后 */ private List<Integer> rightParenthesesIndex; @Override public String toString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < ruleItems.size(); i++) { if (leftParenthesesIndex.contains(i)) { sb.append("("); } sb.append(ruleItems.get(i).toString()); if (rightParenthesesIndex.contains(i)) { sb.append(")"); } if (!ruleItems.get(i).getLogicalOpra().equals(LogicalOpration.None)) { sb.append(" ").append(ruleItems.get(i).getLogicalOpra().getCode()).append(" "); } } return sb.toString(); } } @Data @AllArgsConstructor @NoArgsConstructor @Builder static class SpelResult { private String express; private StandardEvaluationContext context; } private static RuleModel toRuleModel(String expression) { // ({status} in '2,3' or {level} in '1,2') && {hours}>=48 && {phone1}=={phone2} // 1. 从头开始查找 ,直到找到 and/or为止 List<String> ruleExpressions = Arrays.asList(expression.split(" ")); int i = -1; int startIdx = 0; List<RuleItem> collect = new ArrayList<>(); List<Integer> leftParenthesesIndex = new ArrayList<>(); List<Integer> rightParenthesesIndex = new ArrayList<>(); while (++i < ruleExpressions.size()) { if (ruleExpressions.get(i).equals("and") || ruleExpressions.get(i).equals("or")) { String logicalOperation = ruleExpressions.get(i); // 将之前的所有 String ruleExpression = String.join(" ", ruleExpressions.subList(startIdx, i)).trim(); if (ruleExpression.startsWith("(")) { leftParenthesesIndex.add(collect.size()); ruleExpression = ruleExpression.substring(1); } if (ruleExpression.endsWith(")")) { rightParenthesesIndex.add(collect.size()); ruleExpression = ruleExpression.substring(0, ruleExpression.length() - 1); } RuleItem ruleItem = toRuleItem(ruleExpression); startIdx = i + 1; collect.add(ruleItem); if (logicalOperation.equals("and")) { ruleItem.logicalOpra = LogicalOpration.And; } else { ruleItem.logicalOpra = LogicalOpration.Or; } } } if (startIdx < ruleExpressions.size()) { String ruleExpression = String.join(" ", ruleExpressions.subList(startIdx, ruleExpressions.size())).trim(); if (ruleExpression.startsWith("(")) { leftParenthesesIndex.add(collect.size()); ruleExpression = ruleExpression.substring(1); } if (ruleExpression.endsWith(")")) { rightParenthesesIndex.add(collect.size()); ruleExpression = ruleExpression.substring(0, ruleExpression.length() - 1); } RuleItem ruleItem = toRuleItem(ruleExpression); collect.add(ruleItem); } RuleModel model = new RuleModel(); model.setRuleItems(collect); model.setLeftParenthesesIndex(leftParenthesesIndex); model.setRightParenthesesIndex(rightParenthesesIndex); Assert.isTrue(leftParenthesesIndex.size() == rightParenthesesIndex.size(), "括号不能封闭"); return model; } private static RuleItem toRuleItem(String expression) { // {status} in '2,3' RuleItem result = new RuleItem(); List<String> ruleExpressions = Arrays.asList(expression.split(" ")); Assert.isTrue(ruleExpressions.size() == 3, ""); int i = 1; if (ruleExpressions.get(i).equals("in") || ruleExpressions.get(i).equals("==") || ruleExpressions.get(i).equals("!=") || ruleExpressions.get(i).equals(">=") || ruleExpressions.get(i).equals("<=") || ruleExpressions.get(i).equals(">") || ruleExpressions.get(i).equals("<") ) { String comparelOpration = ruleExpressions.get(i); // 将之前的所有 result.left = ruleExpressions.get(0); result.right = ruleExpressions.get(2); if (comparelOpration.equals("==")) { result.comparelOpration = ComparelOpration.Equal; } else if (comparelOpration.equals("!=")) { result.comparelOpration = ComparelOpration.NotEqual; } else if (comparelOpration.equals("in")) { result.comparelOpration = ComparelOpration.In; } else if (comparelOpration.equals("notIn")) { result.comparelOpration = ComparelOpration.NotIn; } else if (comparelOpration.equals(">=")) { result.comparelOpration = ComparelOpration.GreaterEqualThan; } else if (comparelOpration.equals("<=")) { result.comparelOpration = ComparelOpration.LessEqualThan; } else if (comparelOpration.equals(">")) { result.comparelOpration = ComparelOpration.GreaterThan; } else if (comparelOpration.equals("<")) { result.comparelOpration = ComparelOpration.LessThan; } } return result; }
public enum ComparelOpration { In("in"), NotIn("notIn"), GreaterThan(">"), LessThan("<"), GreaterEqualThan(">="), LessEqualThan("<="), Equal("=="), NotEqual("!="); private String code; ComparelOpration(String code) { this.code = code; } public static boolean isDecimalCompareLogicalOpration(ComparelOpration opration) { return opration.ordinal() == ComparelOpration.GreaterThan.ordinal() || opration.ordinal() == ComparelOpration.GreaterEqualThan.ordinal() || opration.ordinal() == ComparelOpration.LessEqualThan.ordinal() || opration.ordinal() == ComparelOpration.LessThan.ordinal(); } public static boolean isEqualLogicalOpration(ComparelOpration opration) { return opration.ordinal() == ComparelOpration.Equal.ordinal() || opration.ordinal() == ComparelOpration.NotEqual.ordinal() ; } public String getCode(){ return this.code; } }
public enum LogicalOpration { None(""), And("and"), Or("or"); private String code; LogicalOpration(String code) { this.code = code; } static String toStr(LogicalOpration logicalOpration) { return logicalOpration.ordinal() == LogicalOpration.None.ordinal() ? "" : (logicalOpration.ordinal() == LogicalOpration.And.ordinal() ? "&&" : "||"); } public String getCode() { return code; } }
public static void main(String[] args) { String expression = "({status} in '2,3' or {level} in '1,2') and ({hours} >= 48) and ({phone1} == {phone2})"; RuleModel model = toRuleModel(expression); System.out.println(model.toString()); /*List<RuleItem> ruleItems = new ArrayList<>(); ruleItems.add(new RuleItem("{status}", ComparelOpration.In, "2,3", LogicalOpration.Or)); ruleItems.add(new RuleItem("{level}", ComparelOpration.In, "1,2", LogicalOpration.And)); ruleItems.add(new RuleItem("{hours}", ComparelOpration.GreaterEqualThan, "48", LogicalOpration.And)); ruleItems.add(new RuleItem("{phone1}", ComparelOpration.Equal, "{phone2}", LogicalOpration.None)); RuleModel model = new RuleModel(); model.setRuleItems(ruleItems); //左括号在0的位置之前 model.setLeftParenthesesIndex(Collections.singletonList(0)); //右括号在1的位置之后 model.setRightParenthesesIndex(Collections.singletonList(1)); //以上表达式相当于 ({status} in '2,3' or {level} in '1,2') && {hours}>=48 && {phone1}=={phone2} */ //1. {phone1} != {phone2} ,结果为false Map<String, String> userFeature1 = ImmutableMap.of("status", "2", "level", "1", "phone1", "13900000000", "phone2", "13900000001", "hours", "66"); boolean computeResult = SpelMatchFactory.compute(model, userFeature1); System.out.println("userFeature1的匹配结果:" + computeResult); //2.{hours} < 48 ,结果为false Map<String, String> userFeature2 = ImmutableMap.of("status", "2", "level", "1", "phone1", "13900000000", "phone2", "13900000001", "hours", "6"); computeResult = SpelMatchFactory.compute(model, userFeature2); System.out.println("userFeature2的匹配结果:" + computeResult); //3. {status} 不在 2,3 中,但是 level 在 1,2中,结果为true Map<String, String> userFeature3 = ImmutableMap.of("status", "1", "level", "1", "phone1", "13900000000", "phone2", "13900000000", "hours", "66"); computeResult = SpelMatchFactory.compute(model, userFeature3); System.out.println("userFeature3的匹配结果:" + computeResult); //4. {status} 不在 2,3 中,且 level 不在 1,2中,结果为false Map<String, String> userFeature4 = ImmutableMap.of("status", "1", "level", "3", "phone1", "13900000000", "phone2", "13900000000", "hours", "66"); computeResult = SpelMatchFactory.compute(model, userFeature4); System.out.println("userFeature4的匹配结果:" + computeResult); //4.一切都匹配,返回true Map<String, String> userFeature5 = ImmutableMap.of("status", "2", "level", "1", "phone1", "13900000000", "phone2", "13900000000", "hours", "66"); computeResult = SpelMatchFactory.compute(model, userFeature5); System.out.println("userFeature5的匹配结果:" + computeResult); }