一、RuleUtil 开发背景
1.1 越来越多,越来越复杂的业务规则
1、规则的应用场景多
2、规则配置的参数类型多(ID、数值、文本、日期等等)
3、规则的参数条件多(大于、小于、等于、包含、不包含、区间等等)
4、规则的结构复杂(父规则、子规则、子场景、与或、优先级、命中数量限制)
1.2 目前规则匹配实现方式,以及对应痛点
现有实现方式使用了许多 if-else 对匹配参数和条件运算符进行判断,结构复杂,可读性较差
痛点一:规则条件组合方式多样,普通实现代码结构容易混乱;
痛点二:同一规则在多个应用场景的复用性较差;
痛点三:每次需求迭代都要改动核心匹配逻辑,效率低,测试成本高。
二、RuleUtil 如何解决痛点
2.1 核心思想
1、通过设计 Rule 结构和四个字段类型,对业务规则进行高度抽象和统一,提高代码可复用性
2、研发仅需实现从业务规则到 Rule 的转换方法,以及定义匹配参数结构即可,无需再编写复杂的匹配逻辑,交给 match 方法通通搞定
RuleUtil 核心规则类(Rule)
/**
* 核心规则类
*/
public class RuleV2 {
// 主规则id
private String id;
// 子规则id
private String subId;
// 规则参数
private Object param;
// 规则字段
private List<Field> fields;
public static class Field {
// 字段名
private String name;
// 字段键
private String key;
// 字段类型
private RuleFieldTypeEnum type;
// 配置条件
private RuleFieldConditionEnum condition;
// 字段值
private Object value;
}
}
RuleUtil 核心匹配方法(match)
/**
* 规则工具类V2
*/
@Slf4j
public class RuleUtilV2 {
/**
* 规则匹配
*
* @param param 待匹配参数
* @param rules 待匹配规则池
* @return 命中规则池
*/
public static List<RuleV2> match(Object param, List<RuleV2> rules) {
return match(param, rules, rules.size());
}
/**
* 规则匹配
*
* @param param 待匹配参数
* @param rules 待匹配规则池
* @param matchLimit 命中规则数量限制
* @return 命中规则池
*/
public static List<RuleV2> match(Object param, List<RuleV2> rules, Integer matchLimit) {
List<RuleV2> matchRules = new ArrayList<>();
if (Objects.isNull(param) || CollUtil.isEmpty(rules) || Objects.isNull(matchLimit) || matchLimit <= NumberConst.ZERO) {
return matchRules;
}
for (RuleV2 rule : rules) {
if (CollUtil.isEmpty(rule.getFields())) {
continue;
}
boolean isMatch = true;
for (RuleV2.Field field : rule.getFields()) {
if (StrUtil.isBlank(field.getName()) || Objects.isNull(field.getType()) || Objects.isNull(field.getCondition()) || Objects.isNull(field.getValue())) {
isMatch = false;
log.info("RuleUtil::match 规则匹配存在空字段 规则id:{} 规则字段:{}", rule.getId(), field);
break;
}
// 根据规则字段名获取参数字段值
Object paramFieldValueObj = ReflectUtil.getFieldValue(param, field.getName());
// 获取规则字段值
Object ruleFieldValueObj = field.getValue();
if (RuleFieldTypeEnum.NORMAL.equals(field.getType())) {
// 普通属性
Set<String> paramFieldValues = collConvertToSet(paramFieldValueObj);
Set<String> ruleFieldValues = collConvertToSet(ruleFieldValueObj);
if (Set.of(RuleFieldConditionEnum.LT, RuleFieldConditionEnum.LE, RuleFieldConditionEnum.GT, RuleFieldConditionEnum.GE).contains(field.getCondition())) {
isMatch = false;
break;
}
if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && !Objects.equals(paramFieldValues, ruleFieldValues)) {
// 等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && Objects.equals(paramFieldValues, ruleFieldValues)) {
// 不等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {
// 包含
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {
// 不包含
isMatch = false;
break;
}
}
if (RuleFieldTypeEnum.NUMBER.equals(field.getType())) {
// 数值属性
if (Set.of(RuleFieldConditionEnum.IN, RuleFieldConditionEnum.NIN).contains(field.getCondition())) {
Set<String> paramFieldValues = collConvertToSet(paramFieldValueObj);
Set<String> ruleFieldValues = collConvertToSet(ruleFieldValueObj);
if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {
// 包含
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {
// 不包含
isMatch = false;
break;
}
continue;
}
String paramFieldValue = String.valueOf(paramFieldValueObj);
if (!NumberUtil.isNumber(paramFieldValue) || !NumberUtil.isNumber(String.valueOf(ruleFieldValueObj))) {
isMatch = false;
log.info("RuleUtil::match 规则匹配存在非数值字段 规则id:{} 规则字段值:{} 参数字段值:{}", rule.getId(), field.getValue(), paramFieldValue);
break;
}
double paramFieldDouble = NumberUtil.parseDouble(paramFieldValue);
double ruleFieldDouble = NumberUtil.parseDouble(String.valueOf(ruleFieldValueObj));
// 数值属性
if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && paramFieldDouble != ruleFieldDouble) {
// 等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && paramFieldDouble == ruleFieldDouble) {
// 不等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.LT.equals(field.getCondition()) && paramFieldDouble >= ruleFieldDouble) {
// 小于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.LE.equals(field.getCondition()) && paramFieldDouble > ruleFieldDouble) {
// 小于等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.GT.equals(field.getCondition()) && paramFieldDouble <= ruleFieldDouble) {
// 大于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.GE.equals(field.getCondition()) && paramFieldDouble < ruleFieldDouble) {
// 大于等于
isMatch = false;
break;
}
}
if (RuleFieldTypeEnum.NORMAL_KV.equals(field.getType()) && StrUtil.isNotBlank(field.getKey())) {
// 普通键值对属性
if (Set.of(RuleFieldConditionEnum.LT, RuleFieldConditionEnum.LE, RuleFieldConditionEnum.GT, RuleFieldConditionEnum.GE).contains(field.getCondition())) {
isMatch = false;
break;
}
Set<String> paramFieldValues = mapConvertToSet(paramFieldValueObj, field.getKey());
Set<String> ruleFieldValues = collConvertToSet(ruleFieldValueObj);
if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && !Objects.equals(paramFieldValues, ruleFieldValues)) {
// 等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && Objects.equals(paramFieldValues, ruleFieldValues)) {
// 不等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {
// 包含
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldValues, ruleFieldValues)) {
// 不包含
isMatch = false;
break;
}
}
if (RuleFieldTypeEnum.NUMBER_KV.equals(field.getType()) && StrUtil.isNotBlank(field.getKey())) {
// 数值键值对属性
if (Set.of(RuleFieldConditionEnum.IN, RuleFieldConditionEnum.NIN).contains(field.getCondition())) {
Set<Double> paramFieldDoubles = mapConvertToSet(paramFieldValueObj, field.getKey()).stream().filter(NumberUtil::isNumber).map(NumberUtil::parseDouble).collect(Collectors.toSet());
Set<Double> ruleFieldDoubles = collConvertToSet(ruleFieldValueObj).stream().filter(NumberUtil::isNumber).map(NumberUtil::parseDouble).collect(Collectors.toSet());
if (RuleFieldConditionEnum.IN.equals(field.getCondition()) && !CollUtil.containsAny(paramFieldDoubles, ruleFieldDoubles)) {
// 包含
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NIN.equals(field.getCondition()) && CollUtil.containsAny(paramFieldDoubles, ruleFieldDoubles)) {
// 不包含
isMatch = false;
break;
}
continue;
}
String paramFieldValue = IterUtil.getFirst(mapConvertToSet(paramFieldValueObj, field.getKey()));
if (!NumberUtil.isNumber(paramFieldValue) || !NumberUtil.isNumber(String.valueOf(ruleFieldValueObj))) {
isMatch = false;
log.info("RuleUtil::match 规则匹配存在非数值字段 规则id:{} 规则字段值:{} 参数字段值:{}", rule.getId(), field.getValue(), paramFieldValue);
break;
}
double paramFieldDouble = NumberUtil.parseDouble(paramFieldValue);
double ruleFieldDouble = NumberUtil.parseDouble(String.valueOf(ruleFieldValueObj));
// 数值属性
if (RuleFieldConditionEnum.EQ.equals(field.getCondition()) && paramFieldDouble != ruleFieldDouble) {
// 等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.NE.equals(field.getCondition()) && paramFieldDouble == ruleFieldDouble) {
// 不等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.LT.equals(field.getCondition()) && paramFieldDouble >= ruleFieldDouble) {
// 小于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.LE.equals(field.getCondition()) && paramFieldDouble > ruleFieldDouble) {
// 小于等于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.GT.equals(field.getCondition()) && paramFieldDouble <= ruleFieldDouble) {
// 大于
isMatch = false;
break;
}
if (RuleFieldConditionEnum.GE.equals(field.getCondition()) && paramFieldDouble < ruleFieldDouble) {
// 大于等于
isMatch = false;
break;
}
}
}
if (isMatch) {
matchRules.add(rule);
if (matchRules.size() >= matchLimit) {
return matchRules;
}
}
}
return matchRules;
}
/**
* 把集合类型对象转换为set
*
* @param obj 对象
* @return set
*/
private static Set<String> collConvertToSet(Object obj) {
Set<String> set = new HashSet<>();
if (Objects.isNull(obj)) {
return set;
}
if (obj instanceof Collection<?> paramFieldColl) {
for (Object subObj : paramFieldColl) {
set.add(String.valueOf(subObj));
}
} else {
set.add(String.valueOf(obj));
}
return set;
}
/**
* 把map类型对象转换为set
*
* @param obj 对象
* @param key 键
* @return set
*/
private static Set<String> mapConvertToSet(Object obj, String key) {
Set<String> set = new HashSet<>();
if (Objects.isNull(obj) || StrUtil.isBlank(key) || !(obj instanceof Map<?, ?> map)) {
return set;
}
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (key.equals(String.valueOf(entry.getKey()))) {
if (entry.getValue() instanceof Collection<?> paramFieldColl) {
for (Object subObj : paramFieldColl) {
set.add(String.valueOf(subObj));
}
} else {
set.add(String.valueOf(entry.getValue()));
}
break;
}
}
return set;
}
}
RuleUtil 把业务规则配置的字段类型,归纳为以下四种(代码枚举:RuleFieldTypeEnum),各个类型支持的场景如下:
/**
* 规则项字段类型枚举
*/
@Getter
@AllArgsConstructor
public enum RuleFieldTypeEnum {
UNKNOWN(0, ""),
NORMAL(1, "普通"),
NUMBER(2, "数值"),
NORMAL_KV(3, "普通键值对"),
NUMBER_KV(4, "数值键值对"),
;
private final Integer code;
private final String desc;
}
2.2 应用场景
应用场景一:列举以下四个业务规则示例,分别对应上面四种字段类型:
第一步:实现 convert 方法,将业务规则转换为 Rule 实体列表
根据示例转换后的 Rule 实体 json 结构如下:
第二步:根据【字段名-Field.name】【字段类型-Field.type】自行定义匹配 Rule 所需要的参数实体类。根据示例定义的 Param 实体结构如下:
/**
* 规则匹配参数
*/
class Param {
// 品类组合id(普通)
private List<Long> combIds;
// 库存数量(数值)
private Long stock;
// 属性id-属性值id(普通键值对)
private Map<Long, List<Long>> attrIdToAttrValIdMap;
// 成分属性值id-属性数值(数值键值对)
private Map<Long, Double> componentAttrValIdToValMap;
}
第三步:调用 RuleUtile.match 方法,得到 Param 命中的 Rule 列表。
根据示例 Param 创建一个待匹配对象
{
"combIds": [
"1-男装",
"3-上装"
],
"stock": 15,
"attrIdToAttrValIdMap": {
"10-适合类型": [
"11-宽松",
"13-修身"
],
"20-织造方式": [
"21-牛仔"
]
},
"componentAttrValIdToValMap": {
"10-棉": 15.0,
"20-尼龙": 85.0
}
}
调用 RuleUtile.match 方法如下,方法返回了能够命中的 Rule
match 方法使用反射,根据【Rule.name】获取 Param 对应字段的值进行条件匹配
应用场景二:假设业务规则发生变化,迭代为嵌套【且】【或】关系的业务规则
第一步:更新 convert 方法,将新的业务规则转换为 Rule 实体列表
根据示例转换后的 Rule 实体 json 结构如下:
⚠️注意:多个【Rule】之间是【或】关系,多个【Rule.Field】之间是【且】关系
第二步:定义的 Param 实体结构不变
第三步:调用 RuleUtile.match 方法如下,方法返回了能够命中的 Rule
场景一、场景二的完整单元测试用例代码:
public class RuleUtilTest {
@Test
public void demo01() {
// 规则匹配参数
TestParam param = new TestParam();
param.setCombIds(List.of(1L, 3L));
param.setStock(15L);
param.setAttrIdToAttrValIdMap(Map.of(10L, List.of(11L, 13L), 20L, List.of(21L)));
param.setComponentAttrValIdToValMap(Map.of(10L, 15.0, 20L, 85.0));
// 品类组合
RuleV2.Field fieldCombIds = new RuleV2.Field();
fieldCombIds.setName(LambdaUtil.getFieldName(TestParam::getCombIds));
fieldCombIds.setType(RuleFieldTypeEnum.NORMAL); // 普通
fieldCombIds.setCondition(RuleFieldConditionEnum.IN); // 包含
fieldCombIds.setValue(List.of(1L, 2L));
RuleV2 rule01 = new RuleV2();
rule01.setId("rule01");
rule01.setFields(List.of(fieldCombIds));
// 库存数量
RuleV2.Field fieldStockGt = new RuleV2.Field();
fieldStockGt.setName(LambdaUtil.getFieldName(TestParam::getStock));
fieldStockGt.setType(RuleFieldTypeEnum.NUMBER); // 数值
fieldStockGt.setCondition(RuleFieldConditionEnum.GT); // 大于
fieldStockGt.setValue("10");
RuleV2.Field fieldStockLt = new RuleV2.Field();
fieldStockLt.setName(LambdaUtil.getFieldName(TestParam::getStock));
fieldStockLt.setType(RuleFieldTypeEnum.NUMBER);
fieldStockLt.setCondition(RuleFieldConditionEnum.LT); // 小于
fieldStockLt.setValue("20");
RuleV2 rule02 = new RuleV2();
rule02.setId("rule02");
rule02.setFields(List.of(fieldStockGt, fieldStockLt));
// 属性id-属性值id(普通键值对)
RuleV2.Field fieldAttrIdToAttrValIdMap = new RuleV2.Field();
fieldAttrIdToAttrValIdMap.setName(LambdaUtil.getFieldName(TestParam::getAttrIdToAttrValIdMap));
fieldAttrIdToAttrValIdMap.setKey("10");
fieldAttrIdToAttrValIdMap.setType(RuleFieldTypeEnum.NORMAL_KV);
fieldAttrIdToAttrValIdMap.setCondition(RuleFieldConditionEnum.IN); // 包含
fieldAttrIdToAttrValIdMap.setValue(List.of(11L, 12L));
RuleV2 rule03 = new RuleV2();
rule03.setId("rule03");
rule03.setFields(List.of(fieldAttrIdToAttrValIdMap));
// 成分属性值id-属性数值
RuleV2.Field fieldComponentAttrValIdToValMapGt = new RuleV2.Field();
fieldComponentAttrValIdToValMapGt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));
fieldComponentAttrValIdToValMapGt.setKey("10");
fieldComponentAttrValIdToValMapGt.setType(RuleFieldTypeEnum.NUMBER_KV); // 数值键值对
fieldComponentAttrValIdToValMapGt.setCondition(RuleFieldConditionEnum.GT); // 大于
fieldComponentAttrValIdToValMapGt.setValue("10");
RuleV2.Field fieldComponentAttrValIdToValMapLt = new RuleV2.Field();
fieldComponentAttrValIdToValMapLt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));
fieldComponentAttrValIdToValMapLt.setKey("10");
fieldComponentAttrValIdToValMapLt.setType(RuleFieldTypeEnum.NUMBER_KV);
fieldComponentAttrValIdToValMapLt.setCondition(RuleFieldConditionEnum.LT); // 小于
fieldComponentAttrValIdToValMapLt.setValue("20");
RuleV2 rule04 = new RuleV2();
rule04.setId("rule04");
rule04.setFields(List.of(fieldComponentAttrValIdToValMapGt, fieldComponentAttrValIdToValMapLt));
// 调用 RuleUtil 规则匹配方法
List<RuleV2> matchResults = RuleUtilV2.match(param, List.of(rule01, rule02, rule03, rule04));
// 命中四种字段类型的规则
assertEquals(List.of(rule01, rule02, rule03, rule04), matchResults);
}
@Test
public void demo02() {
// 规则匹配参数
TestParam param = new TestParam();
param.setCombIds(List.of(1L, 3L));
param.setStock(15L);
param.setAttrIdToAttrValIdMap(Map.of(10L, List.of(11L, 13L), 20L, List.of(21L)));
param.setComponentAttrValIdToValMap(Map.of(10L, 15.0, 20L, 85.0));
// 品类组合
RuleV2.Field fieldCombIds = new RuleV2.Field();
fieldCombIds.setName(LambdaUtil.getFieldName(TestParam::getCombIds));
fieldCombIds.setType(RuleFieldTypeEnum.NORMAL); // 普通
fieldCombIds.setCondition(RuleFieldConditionEnum.IN); // 包含
fieldCombIds.setValue(List.of(1L, 2L));
// 库存数量
RuleV2.Field fieldStockGt = new RuleV2.Field();
fieldStockGt.setName(LambdaUtil.getFieldName(TestParam::getStock));
fieldStockGt.setType(RuleFieldTypeEnum.NUMBER); // 数值
fieldStockGt.setCondition(RuleFieldConditionEnum.GT); // 大于
fieldStockGt.setValue("10");
RuleV2.Field fieldStockLt = new RuleV2.Field();
fieldStockLt.setName(LambdaUtil.getFieldName(TestParam::getStock));
fieldStockLt.setType(RuleFieldTypeEnum.NUMBER);
fieldStockLt.setCondition(RuleFieldConditionEnum.LT); // 小于
fieldStockLt.setValue("20");
RuleV2 prule01_rule01 = new RuleV2();
prule01_rule01.setId("prule01");
prule01_rule01.setSubId("rule01");
prule01_rule01.setFields(List.of(fieldCombIds, fieldStockGt, fieldStockLt));
// 属性id-属性值id(普通键值对)
RuleV2.Field fieldAttrIdToAttrValIdMap = new RuleV2.Field();
fieldAttrIdToAttrValIdMap.setName(LambdaUtil.getFieldName(TestParam::getAttrIdToAttrValIdMap));
fieldAttrIdToAttrValIdMap.setKey("10");
fieldAttrIdToAttrValIdMap.setType(RuleFieldTypeEnum.NORMAL_KV);
fieldAttrIdToAttrValIdMap.setCondition(RuleFieldConditionEnum.IN); // 包含
fieldAttrIdToAttrValIdMap.setValue(List.of(11L, 12L));
// 成分属性值id-属性数值
RuleV2.Field fieldComponentAttrValIdToValMapGt = new RuleV2.Field();
fieldComponentAttrValIdToValMapGt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));
fieldComponentAttrValIdToValMapGt.setKey("10");
fieldComponentAttrValIdToValMapGt.setType(RuleFieldTypeEnum.NUMBER_KV); // 数值键值对
fieldComponentAttrValIdToValMapGt.setCondition(RuleFieldConditionEnum.GT); // 大于
fieldComponentAttrValIdToValMapGt.setValue("10");
RuleV2.Field fieldComponentAttrValIdToValMapLt = new RuleV2.Field();
fieldComponentAttrValIdToValMapLt.setName(LambdaUtil.getFieldName(TestParam::getComponentAttrValIdToValMap));
fieldComponentAttrValIdToValMapLt.setKey("10");
fieldComponentAttrValIdToValMapLt.setType(RuleFieldTypeEnum.NUMBER_KV);
fieldComponentAttrValIdToValMapLt.setCondition(RuleFieldConditionEnum.LT); // 小于
fieldComponentAttrValIdToValMapLt.setValue("20");
RuleV2 prule01_rule02 = new RuleV2();
prule01_rule02.setId("prule01");
prule01_rule02.setSubId("rule02");
prule01_rule02.setFields(List.of(fieldAttrIdToAttrValIdMap, fieldComponentAttrValIdToValMapGt, fieldComponentAttrValIdToValMapLt));
// 调用 RuleUtil 规则匹配方法
List<RuleV2> matchResults = RuleUtilV2.match(param, List.of(prule01_rule01, prule01_rule02));
// 命中组合字段的规则
assertEquals(List.of(prule01_rule01, prule01_rule02), matchResults);
}
}
三、RuleUtil 实践技巧
1、Rule 类的 param 字段可以 set 任何规则信息(如子规则配置的图片 id、枚举值等等);
2、若业务规则存在不同优先级,可以通过对 Rule 列表进行排序实现优先级匹配,排序靠前的优先匹配;
3、若存在命中规则的数量限制(比如优先或随机选择命中的第一个规则),可以通过入参 matchLimit 进行控制;
4、若业务场景对业务规则的更新不要求实时,可以对转换后的 Rule 数组进行缓存,降低数据库查询和转换次数,提高性能;
5、后续将逐步完善 RuleUtil 单元测试用例,保证核心匹配逻辑准确无误。