Aviator规则引擎组件开发

Aviator 规则引擎 是一款轻量级、高性能的 Java 表达式求值引擎,专注于解决动态规则执行和表达式计算问题,它可以运用在如电商促销规则(满减、折扣)、风控策略(用户评分模型)等场景。

困扰:

在过去的项目中,我多次使用规则引擎来处理不同的业务场景。每个项目的规则需求各不相同,前端传递的 JSON 数据格式也多种多样。为了应对这些差异,我不得不为每种规则单独编写解析方法,将前端的 JSON 数据转换为对应的规则公式。为了解决这一问题,我设计了一个通用组件,用于统一前端 JSON 数据的格式,并在后端实现了一套通用的解析逻辑。该组件具有高度的可扩展性和复用性,能够适应不同项目的规则需求,同时减少了重复开发的工作量。

PS: 组件代码处处都重要,篇幅过长,我主要介绍将规则实体解析成公式的部分。

规则 的核心由条件动作两部分组成。首先,需要明确触发规则的条件,即满足何种逻辑时规则会被命中;其次,在规则命中后,需定义执行的具体动作。同时,还需考虑规则未命中时的处理逻辑,以确保规则引擎在不同场景下均能做出合理的响应。

组件整体架构

以下是我统一的规则实体

规则实体

js 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Rule {

    @Schema(description = "规则ID")
    private String id;

    @Schema(description = "规则表达式")
    private String expression;

    @Schema(description = "条件列表")
    private List<Condition> conditions;

    @Schema(description = "命中后的动作")
    private Action action;

    @Schema(description = "未命中的动作")
    private Action fallbackAction;

//    @Schema(description = "规则优先级")
//    private int priority;
//
//    @Schema(description = "规则是否有效")
//    private boolean validity;
}

条件实体

js 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Condition {

    @Schema(description = "字段名")
    private String field;

    @Schema(description = "操作符")   // 可以是 '==', '>', '<' 等
    private String operator;

    @Schema(description = "值")
    private List<String> value;

    @Schema(description = "自定义函数")
    private String customFunction;

    @Schema(description = "布尔逻辑") // 可以是 "&&" 或 "||"
    private String logic;

    @Schema(description = "前缀")
    private String prefix;

    @Schema(description = "值是否被引号包围")
    private Boolean isValueQuoted = false;

    @Schema(description = "子条件")
    private List<Condition> subConditions;

    @Schema(description = "算术表达式链")
    private List<ArithmeticExpression> arithmeticExpressions;

    @Schema(description = "是否需要括号包围")
    private boolean isBracketed;

    @Schema(description = "表达式,存在则直接运用,不解析")
    private String Expression;

    @Schema(description = "命中后的动作")
    private Action action;

动作实体

js 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Action {

    @Schema(description = "动作类型")
    private String type;

    @Schema(description = "动作目标")
    private Object target;

    @Schema(description = "目标类型")
    private Class<?> targetType;
}

算术表达式链(为了满足复杂公式的解析)

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ArithmeticExpression {
    @Schema(description = "算术操作符,例如 + 或 -,可以为null表示第一个操作数")
    private String operator;
    @Schema(description = "操作数,例如 A 或 B")
    private String operand;

    public ArithmeticExpression(String operand) {
        this.operator = null;
        this.operand = operand;
    }
}

规则构造测试

简单加减乘除,与或非

算术表达式链,括号的运用

自定义函数的使用

规则实体解析成公式的方法

java 复制代码
/**
 * 获取完整表达式
 *
 * @param rule
 * @return
 */
public static String buildStatement(Rule rule) {
    String expression;
    if (ObjUtil.isNotNull(rule.getExpression()) && !rule.getExpression().isEmpty()) {
        // 有传表达式,则直接使用,不解析
        expression = rule.getExpression();
    } else {
        // 解析
        expression = buildExpression(rule.getConditions());
    }

    if (Boolean.TRUE.equals(rule.getIsMontageAction())) {
        if (Boolean.TRUE.equals(rule.getIsDegree())) {
            
            // 计算匹配度
            int i = calculateMinConditions(expression);

            return String.format("if (%s) { return %s; } else { return %s; }", expression, i, -1);
        } else {
            return String.format("if (%s) { return %s; } else { return %s; }",
                    expression,
                    rule.getAction().getTarget().toString(),
                    rule.getFallbackAction().getTarget().toString());
        }

    } else {
        return expression;
    }
}

构建规则表达式

java 复制代码
/**
 * 构建规则表达式
 *
 * @param conditions 条件
 * @return 表达式
 */
public static String buildExpression(List<Condition> conditions) {
    StringBuilder expressionBuilder = new StringBuilder();

    // 判断是否存在action有值的condition
    long actionNum = conditions.stream().filter(condition -> ObjUtil.isNotEmpty(condition.getAction())).count();
    if (actionNum > 0) {
        expressionBuilder.append("if (");
    }

    for (int i = 0; i < conditions.size(); i++) {
        Condition condition = conditions.get(i);

        if (ObjUtil.isNotEmpty(condition.getExpression())) {

            expressionBuilder.append("(").append(condition.getExpression()).append(")");

            if (ObjUtil.isNotEmpty(condition.getLogic())) {
                expressionBuilder.append(" ").append(condition.getLogic()).append(" ");
            }

        } else {

            // 如果需要括号,添加左括号
            if (condition.isBracketed()) {
                expressionBuilder.append("(");
            }

            // 处理子条件
            if (condition.getSubConditions() != null && !condition.getSubConditions().isEmpty()) {
                expressionBuilder.append(buildExpression(condition.getSubConditions()));
            }

            // 动态拼接前缀
            String prefixedField = condition.getPrefix() != null ? condition.getPrefix() + condition.getField() : condition.getField();


            // 处理算术表达式或单一条件
            if (condition.getArithmeticExpressions() != null && !condition.getArithmeticExpressions().isEmpty()) {
                for (int j = 0; j < condition.getArithmeticExpressions().size(); j++) {
                    ArithmeticExpression arithmeticExpression = condition.getArithmeticExpressions().get(j);

                    // 如果是第一个操作数,直接添加
                    if (j > 0 && arithmeticExpression.getOperator() != null) {
                        expressionBuilder.append(" ").append(arithmeticExpression.getOperator()).append(" ");
                    }
                    expressionBuilder.append(arithmeticExpression.getOperand());
                }
                // 添加比较运算符和值
                expressionBuilder.append(" ")
                        .append(condition.getOperator())
                        .append(" ")
                        .append(condition.getIsValueQuoted() ? "'" + String.join(", ", condition.getValue()) + "'" : String.join(", ", condition.getValue()));
            } else if (condition.getField() != null) {

                if (condition.getCustomFunction() != null && !condition.getCustomFunction().isEmpty()) {
                    // 使用自定义函数,确保格式为 field operator function(value)
                    expressionBuilder.append(prefixedField)
                            .append(" ")
                            .append(condition.getOperator())
                            .append(" ")
                            .append(condition.getCustomFunction())
                            .append("(")
                            .append(condition.getIsValueQuoted() ? "'" + String.join(", ", condition.getValue()) + "'" : String.join(", ", condition.getValue()))
                            .append(")");
                } else {

                    // 普通的操作符逻辑
                    expressionBuilder.append(prefixedField)
                            .append(" ")
                            .append(condition.getOperator())
                            .append(" ")
                            .append(condition.getIsValueQuoted() ? "'" + String.join(", ", condition.getValue()) + "'" : String.join(", ", condition.getValue()));
                }
            }

            // 如果需要括号,添加右括号
            if (condition.isBracketed()) {
                expressionBuilder.append(")");
            }

            // 添加逻辑操作符
            if (i < conditions.size() - 1 && condition.getLogic() != null) {
                expressionBuilder.append(" ").append(condition.getLogic()).append(" ");
            }
        }
        // 添加逻辑操作符
        if (i < conditions.size() - 1) {
            Condition nextCondition = conditions.get(i + 1);
            if (nextCondition.getAction() != null) {
                expressionBuilder.append(")")
                        .append(" { return ")
                        .append(nextCondition.getAction().getTargetType() == Boolean.class ? nextCondition.getAction().getTarget() : "'" + nextCondition.getAction().getTarget() + "'")
                        .append("; }")
                        .append(" elsif (");
            }
        }
    }
    String result;
    if (expressionBuilder.charAt(expressionBuilder.length() - 1) == '(' && expressionBuilder.length() > 8) {
        result = expressionBuilder.substring(0, expressionBuilder.length() - 8);
    } else {
        result = expressionBuilder.toString().trim();
    }

    return result;
}

实际运用页面图(并未开放复杂规则配置,前端优化一下即可,后台都可以解析)

前端传的规则实体:

js 复制代码
{
  "id": "rule",
  "expression": "",
  "conditions": [
    {
      "field": "",
      "operator": "",
      "value": [
        "aircraftType",
        "'NARROW;WIDE'"
      ],
      "customFunction": "intersect",
      "logic": "&&",
      "prefix": "",
      "isValueQuoted": false,
      "bracketed": false,
      "valueType": "dict",
      "optionalValue": "AircraftType"
    },
    {
      "field": "",
      "operator": "",
      "value": [
        "firstClassVipPaxNum",
        "'121;33'"
      ],
      "customFunction": "intersect",
      "logic": "&&",
      "prefix": "",
      "isValueQuoted": false,
      "bracketed": false,
      "valueType": "",
      "optionalValue": ""
    },
    {
      "field": "firstClassVipPaxNum",
      "operator": ">",
      "value": [
        "'1212'"
      ],
      "customFunction": "",
      "logic": "&&",
      "prefix": "",
      "isValueQuoted": false,
      "bracketed": false,
      "valueType": "",
      "optionalValue": ""
    },
    {
      "field": "",
      "operator": "",
      "value": [
        "airlineIcaoCode",
        "'A320'"
      ],
      "customFunction": "string.endsWith",
      "logic": "",
      "prefix": "",
      "isValueQuoted": false,
      "bracketed": false,
      "valueType": "baseAirport",
      "optionalValue": "airline"
    }
  ],
  "isMontageAction": true,
  "isDegree": true
}

实际转换成的表达式:

js 复制代码
if (intersect(aircraftType, 'NARROW;WIDE') &&   intersect(firstClassVipPaxNum, '121;33') && firstClassVipPaxNum > '1212' &&   string.endsWith(airlineIcaoCode, 'A320')) { return 3; } else { return -1; }

因为最少需要三个参数都命中才可为true,所以匹配度是3(组件里有计算的方法),否则返回 -1,我一个收费项可以配置多个规则,最后返回命中的匹配度最高的规则的计算公式的值。

计算结果(支持批量计算)

总结:

由于开发时间有限,组件的实现可能不够简洁和优雅,但其核心目的是为大家提供一个清晰的思路,帮助大家更好地理解规则引擎的原理及其应用场景。希望通过这个组件,能够启发大家在规则引擎的使用上探索更多可能性。同时,也欢迎大家在此基础上进一步完善和优化,共同打造更加高效、优雅的解决方案。

相关推荐
李长渊哦9 分钟前
Spring Boot 接口延迟响应的实现与应用场景
spring boot·后端·php
Seven9721 分钟前
【设计模式】通过访问者模式实现分离算法与对象结构
java·后端·设计模式
Seven9740 分钟前
【设计模式】遍历集合的艺术:深入探索迭代器模式的无限可能
java·后端·设计模式
小杨4041 小时前
springboot框架项目应用实践五(websocket实践)
spring boot·后端·websocket
浪九天1 小时前
Java直通车系列28【Spring Boot】(数据访问Spring Data JPA)
java·开发语言·spring boot·后端·spring
bobz9651 小时前
IKEv1 和 IKEv2 发展历史和演进背景
后端
大鹏dapeng2 小时前
Gone v2 goner/gin——试试用依赖注入的方式打开gin-gonic/gin
后端·go
tan180°2 小时前
版本控制器Git(1)
c++·git·后端
GoGeekBaird2 小时前
69天探索操作系统-第50天:虚拟内存管理系统
后端·操作系统
_丿丨丨_2 小时前
Django下防御Race Condition
网络·后端·python·django