前情提要:我最近在做项目组件化,趁着这个机会,我使用规则引擎重构了一下以前的模块,接下来我将会重构前后的设计作对比。
资质模块(寻找出符合匹配条件的航班,这些航班被配置了资质的任务只有在已选资质的用户列表中的用户才能执行,其他用户没有资质执行这些任务)

资质部分:

资质规则部分:

额外条件是匹配规则的主体。基本信息符合规则后需要去联表查询的结果。
原项目的数据库设计:

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

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

condition这张表是条件排列组合之后的结果,比如其他条件都选了全部,进港停机位选了 217,218;出港停机位选了228,229;那么就会产生以下4条数据。
| 其他字段 | 进港停机位 | 出港停机位 |
|---|
| - | 217 | 228 |
| - | 217 | 229 |
| - | 218 | 228 |
| - | 218 | 229 |
所以原设计的模块,一条规则会产生非常巨大的数据,存储跟查询都很麻烦。
重构的模块数据库设计

新设计摒弃了原来的condition表,将额外条件以json的形式存储,并将其解析成可以被规则引擎执行的公式 condition_formula。condition_trigger是用于选择解析规则方案的枚举值。
代码部分(只展示资质规则,norm部分,其他不重要)
新增资质规则
@Transactional(rollbackFor = Exception.class)
public void createNormRule(NormVO normVO) {
String normCode = IdUtil.getSnowflakeNextIdStr();
Date now = new Date();
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);
}
public void setConf(Norm norm) {
norm.setConditionFormula(getFormula(norm.getConditionTrigger(), norm.getConditionFormula(), norm.getConditionConf()));
}
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 "";
}
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();
}
private static String parseCondition(ParamComponentVO vo) {
if (vo.getKey() == null || vo.getValue() == null) {
return "";
}
String values = vo.getValue();
String[] valueArray = values.split(";");
String formattedValues = String.join(";", valueArray);
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的时候代表没命中规则。
根据航班信息查询资质规则产生的结果:
public List<FlightWorkerNormVO> getWorkerNorms(List<FlightInfoVO> flightInfoVOS) {
if (CollUtil.isEmpty(flightInfoVOS)) {
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<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;
}
public FlightWorkerNormVO getWorkerNorm(FlightInfoVO flightInfoVO, List<Norm> norms) {
Map<String, Object> infoMap = BeanUtil.beanToMap(flightInfoVO);
List<Norm> filteredNorms = norms.parallelStream()
.filter(norm -> {
int degree = Integer.parseInt(AviatorEvaluator.execute(norm.getConditionFormula(), infoMap).toString());
return degree >= 0;
})
.collect(Collectors.toList());
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();
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 = 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;
}
执行完方法后得到的结果:

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