使用AviatorEvaluator规则引擎将上千条规则条件数据缩减成一个公式

3,373 阅读10分钟

前情提要:我最近在做项目组件化,趁着这个机会,我使用规则引擎重构了一下以前的模块,接下来我将会重构前后的设计作对比。

资质模块(寻找出符合匹配条件的航班,这些航班被配置了资质的任务只有在已选资质的用户列表中的用户才能执行,其他用户没有资质执行这些任务)

image-20240801225112785.png

资质部分:

image-20240801230100306.png

资质规则部分:

image-20240801230221085.png

额外条件是匹配规则的主体。基本信息符合规则后需要去联表查询的结果。

原项目的数据库设计:

image-20240801230840358.png

PS:我用的另一个规则模块的数据(一样的设计)

image-20240801231839892.png

上述条件规则产生的规则数据:

image-20240801231801711.png

condition这张表是条件排列组合之后的结果,比如其他条件都选了全部,进港停机位选了 217,218;出港停机位选了228,229;那么就会产生以下4条数据。

其他字段进港停机位出港停机位
-217228
-217229
-218228
-218229

所以原设计的模块,一条规则会产生非常巨大的数据,存储跟查询都很麻烦。


重构的模块数据库设计

image-20240801232918292.png

新设计摒弃了原来的condition表,将额外条件以json的形式存储,并将其解析成可以被规则引擎执行的公式 condition_formula。condition_trigger是用于选择解析规则方案的枚举值。

代码部分(只展示资质规则,norm部分,其他不重要)

新增资质规则


/**
     * 创建资质规则
     * @param normVO
     */
    @Transactional(rollbackFor = Exception.class)
    public void createNormRule(NormVO normVO) {
        String normCode = IdUtil.getSnowflakeNextIdStr();
        Date now = new Date();

        // 创建 NormPapers 实体列表
        List<NormPapers> normPapersList = normVO.getPapersCodes().stream().map(papersCode -> {
            return NormPapers.builder()
                    .id(IdUtil.getSnowflakeNextId())
                    .normCode(normCode)
                    .papersCode(papersCode)
                    .insertTime(now)
                    .updateTime(now)
                    .build();
        }).collect(Collectors.toList());

        Norm norm = new Norm();
        BeanUtil.copyProperties(normVO, norm);
        norm.setCode(normCode).setInsertTime(now).setUpdateTime(now);

        // 配置公式
        setConf(norm);

        normPapersService.saveBatch(normPapersList);
        this.save(norm);
    }
    
    
    /**
     * 配置公式
     * @param norm
     */
    public void setConf(Norm norm) {
        norm.setConditionFormula(getFormula(norm.getConditionTrigger(), norm.getConditionFormula(), norm.getConditionConf()));
    }
    
    
    /**
     * 获取计算公式
     * @param trigger 触发方式
     * @param formula 公式
     * @param confList 配置
     * @return
     */
    private String getFormula(String trigger, String formula, List<List<ParamComponentVO>> confList) {

        TriggerEnum triggerEnum = TriggerEnum.getEnumByCode(trigger);
        if (triggerEnum == null) {
            return "";
        }

        switch (triggerEnum) {
            case PARAM: // 参数计算
                if (ObjUtil.isNull(confList)) {
                    return "0";
                }
                return generateConditionFormula(confList);

            case TASK_CODE: // 任务节点
                break;
            case PREDICTIVE: // 预测算法
                break;
            case FLIGHT_TIME: // 航班时刻
                break;
            case PRE_SCHEDULE_TIME: // 预分配时间,任务下发
                break;
            default:
        }
        return "";
    }

	/**
     * 生成条件公式
     * @param conditionConf
     * @return
     */
    public static String generateConditionFormula(List<List<ParamComponentVO>> conditionConf) {
        StringBuilder formula = new StringBuilder();

        // 条件个数,用于对比匹配度。
        int num = 0;
        formula.append("if (");

        List<String> conditions = new ArrayList<>();
        for (List<ParamComponentVO> voList : conditionConf) {
            num = voList.size();
            for (ParamComponentVO vo : voList) {
                String condition = parseCondition(vo);
                if (!condition.isEmpty()) {
                    conditions.add(condition);
                }
            }
        }

        formula.append(String.join(" && ", conditions));
        formula.append(") {\n");
        formula.append("\treturn ");
        formula.append(num);
        formula.append(";\n");
        formula.append("} else {\n");
        formula.append("\treturn -1;\n");
        formula.append("}");

        return formula.toString();
    }

    /**
     * 单个条件解析
     * @param vo
     * @return
     */
    private static String parseCondition(ParamComponentVO vo) {
        if (vo.getKey() == null || vo.getValue() == null) {
            return "";
        }

        String values = vo.getValue();
        
        // 使用分号作为分隔符,将值转换为适用于 intersect 方法的格式
        String[] valueArray = values.split(";");
        String formattedValues = String.join(";", valueArray);

        // 生成 intersect 语法的条件
        return String.format("intersect(%s, '%s')", vo.getKey(), formattedValues);
    }

TriggerEnum枚举(用于选择解析规则方案):

@AllArgsConstructor
@Getter
public enum TriggerEnum {

    TASK_CODE("taskNode", "任务节点"),
    FLIGHT_TIME("flightTime", "航班时刻"),
    FEEDBACK("feedback", "反馈结束"),
    PRE_SCHEDULE_TIME("preScheduleTime", "预估分配时间"),
    PREDICTIVE("predictive", "预测算法"),
    PARAM("param", "参数计算"),
    ;

    private String code;
    private String name;

    /**
     * 判断参数合法性
     */
    public static boolean isValid(String code) {
        return Arrays.stream(TriggerEnum.values()).anyMatch(s -> s.code.equals(code));
    }

    public static TriggerEnum getEnumByCode(String code) {
        return Arrays.stream(TriggerEnum.values())
                .filter(s -> Objects.equals(s.getCode(), code))
                .findFirst().
                orElse(null);
    }
}

NormVO

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@Builder
public class NormVO extends Model<NormVO> {

    @Schema(description = "编号")
    private Long id;

    @Schema(description = "有效性")
    @NotNull(message = "有效性不能为空")
    private Boolean validity;

    @Schema(description = "代码")
    @JsonIgnore
    private String code;

    @Schema(description = "名称")
    @NotNull(message = "名称不能为空")
    private String name;

    @Schema(description = "开始时间")
    @NotNull(message = "开始不能为空")
    private Date beginDate;

    @Schema(description = "结束时间")
    @NotNull(message = "结束不能为空")
    private Date endDate;

    @Schema(description = "保障任务")
    @NotNull(message = "保障任务不能为空")
    private String job;

    @Schema(description = "保障目标")
    @NotNull(message = "保障目标不能为空")
    private String jobTarget;

    @Schema(description = "优先级")
    @NotNull(message = "优先级不能为空")
    private Integer priority;

    @Schema(description = "备注")
    private String remark;

    @Schema(description = "涉及资质")
    private List<String> papersCodes;

    @Schema(name = "条件配置触发方式")
    @EnumValue(enumClass = TriggerEnum.class, message = "不支持的条件配置触发方式", enumMethod = "isValid")
    private String conditionTrigger;

    @Schema(name = "条件配置")
    @TableField(typeHandler = JacksonTypeHandler.class)
    private List<List<ParamComponentVO>> conditionConf;

    @Schema(name = "条件配置计算公式")
    private String conditionFormula;
    
    }

ParamComponentVO

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@Schema(name = "参数组件")
@Slf4j
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ParamComponentVO {
    @Schema(name = "标题")
    private String title;

    @Schema(name = "参数名")
    private String key;

    @Schema(name = "辅助参数名")
    private String extraKey;

    @Schema(name = "单位")
    private String unit;

    @Schema(name = "值")
    private String value;

    @Schema(name = "是否显示")
    private Boolean isDisplay;

    @Schema(name = "组件类型")
    private ComponentTypeVO componentType;

    @Schema(name = "子组件")
    private List<ParamComponentVO> subComponents;

    @Schema(name = "子组件List")
    private List<List<ParamComponentVO>> subComponentsList;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Accessors(chain = true)
    @Schema(name = "组件类型")
    public static class ComponentTypeVO {
        @Schema(name = "组件名称")
        private String name;

        @Schema(name = "组件值")
        private Object value;
    }
  }

接口入参:

{
"validity":true,
"name":"资质规则1",
"beginDate":"2024-07-31 00:00:00",
"endDate":"2024-08-30 00:00:00",
"job":"qyc",
"jobTarget":"-",
"priority": 10,
"remark": "备注xxx",
"papersCodes":["1818567392084213760","1818573534780833792"],
"conditionTrigger":"param",
"conditionConf": [
    [
        {
            "title": "保障类型",
            "key": "safeguardType",
            "value": "A;AD"
        },
        {
            "title": "航空公司",
            "key": "airline",
            "value": "HU;3U"
        }
    ]
]

}

产生的数据库单条数据:

condition_conf 字段

[
    [
        {
            "title": "保障类型",
            "key": "safeguardType",
            "value": "A;AD"
        },
        {
            "title": "航空公司",
            "key": "airline",
            "value": "HU;3U"
        }
    ]
]

condition_formula字段

if (intersect(safeguardType, 'A;AD') && intersect(airline, 'HU;3U')) {
	return 2;
} else {
	return -1;
}

condition_trigger字段:

param

自定义规则引擎 intersect函数

public class IntersectFunction extends AbstractFunction {
    /**
     * 自定义函数名称
     */
    @Override
    public String getName() {
        return "intersect";
    }

    @Override
    public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {

        // 获取第一个参数,即列表
        Object list = FunctionUtils.getJavaObject(arg1, env);

        // 获取第二个参数,即要检查的元素列表,并解析为数组
        Object element;
        if (arg2 instanceof AviatorString) {
            element = arg2.getValue(env);
        } else {
            element = FunctionUtils.getJavaObject(arg2, env);
        }
        Set<String> elements = new HashSet<>(Arrays.asList(element.toString().split(";")));

        // 检查列表和元素是否相交
        if (list instanceof Object[]) {
            for (Object item : (Object[]) list) {
                if (elements.contains(item.toString().trim())) {
                    return AviatorBoolean.TRUE;
                }
            }
        } else if (list instanceof Iterable) {
            for (Object item : (Iterable<?>) list) {
                if (elements.contains(item.toString().trim())) {
                    return AviatorBoolean.TRUE;
                }
            }
        } else if (list instanceof String) {

            Set<String> listStr = new HashSet<>(Arrays.asList(list.toString().split(";")));
            for (Object item : listStr) {
                if (elements.contains(item.toString().trim())) {
                    return AviatorBoolean.TRUE;
                }
            }
        }

        return AviatorBoolean.FALSE;
    }
    
        public static void main(String[] args) {
            
		// ';'是切割符
        String formula = "intersect(type, 'a;b') && intersect(airline,'HU')";
        Map<String, Object> env = new HashMap<>();
        env.put("type", new String[]{"a", "b", "c"});
        env.put("airline", "HU;HC");

        // 注册函数
        AviatorEvaluator.addFunction(new IntersectFunction());
        System.out.println("result = " + AviatorEvaluator.execute(formula, env));
        AviatorEvaluator.removeFunction("intersect");
    }
    
  }

intersect函数:判断第一个参数是否跟右侧的字符串切割成列表后会有相交。

以下json代表着规则是 额外条件 保障类型只有在A、AD中,并且满足航空公司在HU、3U中的才能命中这条规则。

[
    [
        {
            "title": "保障类型",
            "key": "safeguardType",
            "value": "A;AD"
        },
        {
            "title": "航空公司",
            "key": "airline",
            "value": "HU;3U"
        }
    ]
]

所以我们在前面解析出来的公式

if (intersect(safeguardType, 'A;AD') && intersect(airline, 'HU;3U')) {
	return 2;
} else {
	return -1;
}

safeguardType与A,AD相交时,代表着航班满足了保障类型这个条件,当所有条件都满足时,我们会返回条件的个数,这个值是匹配度,值越大越匹配,所有额外条件都是全部的时候,condition_formula字段存储的是 0;匹配度也就是0(意味着所有航班都会命中) -1的时候代表没命中规则。

根据航班信息查询资质规则产生的结果:

/**
     * 获取航班对应任务满足资质的人员列表
     * @param flightInfoVOS
     * @return
     */
    public List<FlightWorkerNormVO> getWorkerNorms(List<FlightInfoVO> flightInfoVOS) {

        if (CollUtil.isEmpty(flightInfoVOS)) {
//            return new ArrayList<>();
			
            // 自测时内部获取的数据,正式使用时会删掉
            // 内部联表时做了进出港航班同条件信息拼接  CONCAT(arr.aircraft_category, ';', dep.aircraft_category) as aircraftCategory 
            flightInfoVOS = this.baseMapper.getFlightInfoByFlightIds(new ArrayList<>());

            if (CollUtil.isEmpty(flightInfoVOS)) {
                return new ArrayList<>();
            }
        }

        List<FlightWorkerNormVO> flightWorkerNormVOList = Collections.synchronizedList(new ArrayList<>(16));

        // 获取所有资质规则
        List<Norm> norms = this.baseMapper.getNormList();

        List<Norm> finalNorms = norms.stream().toList();

        AviatorEvaluator.addFunction(new IntersectFunction());


        List<CompletableFuture<FlightWorkerNormVO>> futureList = new ArrayList<>(16);

        flightInfoVOS.forEach(flightInfoVO -> {
            CompletableFuture<FlightWorkerNormVO> future = CompletableFuture.supplyAsync(() -> getWorkerNorm(flightInfoVO, finalNorms), virtualExecutor)
                    .exceptionally(e -> {
                        log.error(e.getMessage(), e);

                        return FlightWorkerNormVO.builder().flightId(flightInfoVO.getFlightId()).jobWorkerNorms(new ArrayList<>()).build();
                    });
            futureList.add(future);
        });

        try {
            // 等待所有的 CompletableFuture 完成
            CompletableFuture<Void> allOf = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
            allOf.join();

        } finally {
            AviatorEvaluator.removeFunction("intersect");
        }

        futureList.forEach(f -> {
            try {
                FlightWorkerNormVO flightWorkerNormVO = f.get();
                if (ObjUtil.isNotEmpty(flightWorkerNormVO)) {
                    flightWorkerNormVOList.add(flightWorkerNormVO);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }
        });


        return flightWorkerNormVOList;
    }

    /**
     * 获取单个航班对应任务满足资质的人员列表
     * @param flightInfoVO
     * @param norms
     * @return
     */
    public FlightWorkerNormVO getWorkerNorm(FlightInfoVO flightInfoVO, List<Norm> norms) {

        Map<String, Object> infoMap = BeanUtil.beanToMap(flightInfoVO);

        // 过滤并收集 degree >= 0 的 Norm 列表(degree >= 0 代表着命中规则)
        List<Norm> filteredNorms = norms.parallelStream()
                .filter(norm -> {
                    int degree = Integer.parseInt(AviatorEvaluator.execute(norm.getConditionFormula(), infoMap).toString());

                    return degree >= 0;
                })
                .collect(Collectors.toList());

        // 以Job为key,合并workerCodeList (目前没有考虑优先级、匹配度,大家可以根据业务需要来做处理)
        Map<String, List<Norm>> groupedByJob = filteredNorms.stream()
                .collect(Collectors.groupingBy(Norm::getJob));

        List<JobWorkerNorm> jobWorkerNorms = groupedByJob.entrySet().stream()
                .map(entry -> {
                    String jobCode = entry.getKey();
                    List<Norm> jobNorms = entry.getValue();

                    // 合并 workerCodeList
                    List<String> workerCodes = jobNorms.stream()
                            .flatMap(norm -> norm.getWorkerCodeList().stream())
                            .distinct()
                            .collect(Collectors.toList());

                    JobWorkerNorm jobWorkerNorm = new JobWorkerNorm();
                    jobWorkerNorm.setJobCode(jobCode);
                    jobWorkerNorm.setWorkerCodes(workerCodes);
                    return jobWorkerNorm;
                })
                .collect(Collectors.toList());

        // 创建 FlightWorkerNormVO 对象
        FlightWorkerNormVO flightWorkerNormVO = new FlightWorkerNormVO();
        flightWorkerNormVO.setFlightId(flightInfoVO.getFlightId());
        flightWorkerNormVO.setJobWorkerNorms(jobWorkerNorms);

        return flightWorkerNormVO;
    }

FlightWorkerNormVO实体

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FlightWorkerNormVO {

    @Schema(description = "航班ID")
    private Long flightId;

    @Schema(description = "任务对应的人员资质列表")
    private List<JobWorkerNorm> jobWorkerNorms;

}

JobWorkerNorm实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class JobWorkerNorm {

    @Schema(description = "任务编码")
    private String jobCode;

    @Schema(description = "有资质的用户列表")
    private List<String> workerCodes;
}

执行完方法后得到的结果:

image-20240802001145484.png

规则引擎的优点:

  • 灵活性:规则可以在不修改代码的情况下轻松添加、删除或修改,适应不断变化的业务需求。
  • 可维护性:业务规则集中管理,代码和规则分离,提高了系统的可维护性和可读性。
  • 可重用性:同一套规则可以应用于多个系统或模块,提高了规则的重用性。
  • 透明性:规则清晰明了,业务人员和开发人员都可以理解和管理,便于沟通和协作。
  • 响应速度快:规则引擎通常经过优化,能够快速评估和执行规则,提升系统响应速度。
  • 一致性:集中管理规则确保了规则执行的一致性,避免了重复定义和执行不一致的问题。

AviatorEvaluator规则引擎内置函数介绍

总结:目前我贴的代码不是很精简,例如TriggerEnum,贴上来只是为了展示一下,好让大家可以参考扩展。当碰到规则时,不妨考虑一下使用规则引擎来优化架构,如果内置函数不满足的话,可以自定义函数去满足各个规则需。(有什么疑问或建议,欢迎在评论区讨论)


更新(紧急更新)2024/8/6

使用 ParamComponentVO 存储json的实体需要添加 autoResultMap = true,才能在查询的时候将json转换回ParamComponentVO

Norm实体

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@Builder
@TableName( value = "bcq_norm", autoResultMap = true)
@Schema(description = "资质规则")
public class Norm extends Model<Norm> {
    @TableId
    @Schema(description = "编号")
    private Long id;

    @Schema(description = "有效性")
    @NotNull(message = "有效性不能为空")
    private Boolean validity;

    @Schema(description = "名称")
    @NotNull(message = "名称不能为空")
    private String name;

    @Schema(description = "开始时间")
    @NotNull(message = "开始不能为空")
    private Date beginDate;

    @Schema(description = "结束时间")
    @NotNull(message = "结束不能为空")
    private Date endDate;

    @Schema(description = "保障任务")
    @NotNull(message = "保障任务不能为空")
    private String job;

    @Schema(description = "保障目标")
    @NotNull(message = "保障目标不能为空")
    private String jobTarget;

    @Schema(description = "优先级")
    @NotNull(message = "优先级不能为空")
    private Integer priority;

    @Schema(name = "条件配置触发方式")
    @EnumValue(enumClass = TriggerEnum.class, message = "不支持的条件配置触发方式", enumMethod = "isValid")
    private String conditionTrigger;

    @Schema(name = "条件配置")
    @TableField(typeHandler = JacksonTypeHandler.class)
    private List<List<ParamComponentVO>> conditionConf;

    @Schema(name = "条件配置计算公式")
    private String conditionFormula;

    @Schema(description = "插入时间")
    @NotNull(message = "插入时间不能为空")
    private Date insertTime;

    @Schema(description = "更新时间")
    @NotNull(message = "更新时间不能为空")
    private Date updateTime;

    @Schema(name = "备注")
    private String remark;

    @Schema(description = "人员编码拼接字符串")
    @TableField(exist = false)
    @JsonIgnore
    private String workerIdStr;

    @Schema(description = "人员编码列表")
    @TableField(exist = false)
    private List<Long> workerIdList;

    public List<Long> getWorkerIdList() {

        List<Long> workerCodeList = new ArrayList<>();
        if (workerIdStr != null) {
            workerCodeList = Arrays.asList(workerIdStr.split(","))
                    .stream()
                    .map(Long::parseLong)
                    .collect(Collectors.toList());
        }
        return workerCodeList;
    }

    @Schema(description = "是否已配置,但不在有效期")
    @TableField(exist = false)
    private Boolean isExpired;

    public Boolean getExpired() {
        Date currentDay = DateUtils.getCurrentDay();

        return beginDate.compareTo(currentDay) <=0 && endDate.compareTo(currentDay) >= 0 ? false : true;
    }
}