基于 JSQLParser 的动态规则引擎实现—订单系统字段驱动的自动标记机制

1,513 阅读9分钟

🔥 动态标记黑科技:SQL 表达式 + 切面编程实现灵活规则引擎

本文将分享一种优雅的解决方案 —— 通过 SQL 表达式结合 Spring AOP 切面编程,实现无需修改代码即可动态给订单配置标记规则的轻量级引擎。

🚀 需求背景:从简单判断到灵活规则的进化

想象这样一个场景:某天老板提出需求:"当订单更新时,需要根据字段值自动打上对应标记"。最初的想法非常简单,不过是几个 if-else 判断:

java

if (order.getProductId().equals("4000001")) {
    order.addMark("重要产品");
}
if (orderRate.getServerRate() > 90) {
    orderRate.addMark("高评分服务");
}

但看到具体要打的密密麻麻的标记,陷入了沉思:

  • 业务要求支持多个 多张表、多个字段的组合条件,比如为创建人是某些用户的单子打上【合作伙伴】的标记,评分为差评的打上【差】标记等等

  • 运营人员希望能在后台直接配置规则,无需开发介入

面对这种 "规则频繁变更" 的典型场景,传统硬编码方式会导致:

  • 代码臃肿:标记的规则可能经常出现变动,每次新增/修改规则都要修改核心逻辑
  • 维护困难:条件组合复杂时难以理清逻辑,写死的逻辑无法快速响应需求变化
  • 扩展性差:无法支持运营人员自助配置,标记的规则可能经常出现变动
  • 缺乏灵活性:不同业务部门、不同客户可能需要不同的标记规则

🛠️ 解决方案:SQL 表达式 + Spring AOP 的黄金组合

经过技术选型,我们决定采用 "SQL 表达式 + 切面编程" 的方案,核心优势在于:

  • 规则动态化:使用 SQL 语法作为规则表达式,支持复杂条件组合

  • 非侵入式:通过 Spring AOP 切面拦截订单更新操作,不修改原有业务逻辑

  • 易维护性:规则配置与代码实现分离,运营人员可自助管理

  • 轻量级:配置简单,不依赖复杂的框架或中间件,功能可快速迭代上线

整体实现思路如下:

  1. 用户在前端输入 SQL 风格的规则表达式
  2. 使用 JSqlParser 解析表达式为抽象语法树
  3. 通过 Spring AOP 拦截订单、订单评分表的更新方法
  4. 构建包含相关数据的上下文对象
  5. 执行规则匹配,符合条件则添加标记

📦核心组件实现:从设计到代码的完整落地

1. RuleContext:数据访问上下文容器

首先需要创建一个数据访问上下文,用于封装相关业务对象,并提供统一的字段访问接口:

java

package com.example.system.api.domain;

import com.example.common.core.utils.sql.FieldAccessor;
import com.example.system.api.domain.table.TableA;
import com.example.system.api.domain.table.TableB;

import java.lang.reflect.Field;

/**
 * 规则执行上下文 - 封装业务数据并提供字段访问接口
 */
public class RuleContext implements FieldAccessor {
    private TableA tableA;
    private TableB tableB;

    public RuleContext(TableA tableA, TableB tableB) {
        this.tableA = tableA;
        this.tableB = tableB;
    }

    @Override
    public Object getFieldValue(String fieldName) {
        // 处理带TableA前缀的字段
        if (fieldName.startsWith("tableA.")) {
            String subField = fieldName.substring("tableA.".length());
            return getFieldFromObject(tableA, subField);
        }
        // 处理带TableB前缀的字段
        else if (fieldName.startsWith("tableB.")) {
            String subField = fieldName.substring("tableB.".length());
            return getFieldFromObject(tableB, subField);
        }
        // 无前缀字段默认从TableA获取
        else if (!fieldName.contains(".")) {
            return getFieldFromObject(tableA, fieldName);
        }
        return null;
    }

    /**
     * 从对象中反射获取字段值
     */
    private Object getFieldFromObject(Object obj, String fieldName) {
        try {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (NoSuchFieldException e) {
            throw new IllegalArgumentException("未知字段: " + fieldName, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("无法访问字段: " + fieldName, e);
        }
    }
}

这个上下文容器实现了FieldAccessor接口,支持通过 "表名.字段名" 的方式访问数据,例如:

  • tableA.productId - 访问 TableA 中的 productId 字段
  • tableB.serverRate - 访问 TableB 中的 serverRate 字段

2. JSqlParserUtil:SQL 表达式解释器核心

接下来是关键的 SQL 表达式解析与求值组件,它负责将用户输入的 SQL 表达式转换为可执行的逻辑判断:

java

public class JSqlParserUtil {

    /**
     * 计算SQL表达式的值
     * @param expression SQL条件表达式
     * @param fieldAccessor 字段访问器
     * @return 表达式计算结果
     */
    public static boolean evaluate(String expression, FieldAccessor fieldAccessor) {
        try {
            // 解析SQL条件表达式
            Expression parsedExpression = CCJSqlParserUtil.parseCondExpression(expression);
            // 计算表达式值
            return evaluateExpression(parsedExpression, fieldAccessor);
        } catch (Exception e) {
            throw new RuntimeException("表达式解析失败: " + expression, e);
        }
    }

    /**
     * 递归计算表达式值
     */
    private static boolean evaluateExpression(Expression expression, FieldAccessor fieldAccessor) {
        // 处理括号嵌套
        if (expression instanceof Parenthesis) {
            return evaluateExpression(((Parenthesis) expression).getExpression(), fieldAccessor);
        }
        // 处理AND逻辑
        else if (expression instanceof AndExpression) {
            return evaluateExpression(((AndExpression) expression).getLeftExpression(), fieldAccessor)
                    && evaluateExpression(((AndExpression) expression).getRightExpression(), fieldAccessor);
        }
        // 处理OR逻辑
        else if (expression instanceof OrExpression) {
            return evaluateExpression(((OrExpression) expression).getLeftExpression(), fieldAccessor)
                    || evaluateExpression(((OrExpression) expression).getRightExpression(), fieldAccessor);
        }
        // 处理等于条件
        else if (expression instanceof EqualsTo) {
            return compare((BinaryExpression) expression, fieldAccessor, Object::equals);
        }
        // 处理不等于条件
        else if (expression instanceof NotEqualsTo) {
            return compare((BinaryExpression) expression, fieldAccessor, (a, b) -> !a.equals(b));
        }
        // 处理大于条件
        else if (expression instanceof GreaterThan) {
            return compare((BinaryExpression) expression, fieldAccessor, (a, b) -> compareValues(a, b) > 0);
        }
        // 处理小于条件
        else if (expression instanceof MinorThan) {
            return compare((BinaryExpression) expression, fieldAccessor, (a, b) -> compareValues(a, b) < 0);
        }
        // 处理大于等于条件
        else if (expression instanceof GreaterThanEquals) {
            return compare((BinaryExpression) expression, fieldAccessor, (a, b) -> compareValues(a, b) >= 0);
        }
        // 处理小于等于条件
        else if (expression instanceof MinorThanEquals) {
            return compare((BinaryExpression) expression, fieldAccessor, (a, b) -> compareValues(a, b) <= 0);
        }
        // 处理IN条件
        else if (expression instanceof InExpression) {
            return handleInExpression((InExpression) expression, fieldAccessor);
        }
        else {
            throw new UnsupportedOperationException("不支持的表达式类型: " + expression);
        }
    }

    /**
     * 处理IN表达式
     */
    private static boolean handleInExpression(InExpression inExpression, FieldAccessor fieldAccessor) {
        Expression leftExpression = inExpression.getLeftExpression();
        ItemsList rightItemsList = inExpression.getRightItemsList();

        // 验证左侧必须是字段
        if (!(leftExpression instanceof Column)) {
            throw new IllegalArgumentException("IN表达式的左侧必须是字段名");
        }

        Column column = (Column) inExpression.getLeftExpression();
        // 获取字段路径(如tableA.productId)
        String param = getFieldPath(column);

        Object fieldValue = fieldAccessor.getFieldValue(param);
        if (fieldValue == null) {
            return false;
        }

        // 验证右侧必须是值列表
        if (rightItemsList == null) {
            throw new IllegalArgumentException("IN表达式的右侧必须是值列表");
        }

        // 处理表达式列表
        if (rightItemsList instanceof ExpressionList) {
            List<Expression> expressions = ((ExpressionList) rightItemsList).getExpressions();
            for (Expression expr : expressions) {
                Object value = getRightValue(expr);
                if (value != null && value.equals(fieldValue)) {
                    return true;
                }
            }
        } else {
            throw new IllegalArgumentException("不支持的值列表类型: " + rightItemsList.getClass());
        }

        return false;
    }

    /**
     * 比较二元表达式
     */
    private static boolean compare(BinaryExpression binaryExpression, FieldAccessor fieldAccessor, BiPredicate<Object, Object> predicate) {
        try {
            Column column = (Column) binaryExpression.getLeftExpression();
            String param = getFieldPath(column);
            Expression rightExpression = binaryExpression.getRightExpression();

            // 获取左侧字段值
            Object leftValue = fieldAccessor.getFieldValue(param);
            if (leftValue == null) {
                return false;
            }

            // 获取右侧表达式值
            Object rightValue = getRightValue(rightExpression);

            // 类型转换处理(如Integer转Long)
            if (leftValue instanceof Integer) {
                leftValue = Long.parseLong(leftValue.toString());
            }

            return predicate.test(leftValue, rightValue);
        } catch (Exception e) {
            throw new IllegalArgumentException("表达式值比较失败: " + binaryExpression, e);
        }
    }

    /**
     * 获取字段完整路径(表名.字段名)
     */
    private static String getFieldPath(Column column) {
        if (column == null) {
            throw new IllegalArgumentException("列对象不能为null");
        }

        String leftField = column.getColumnName();
        String tableName = null;

        if (column.getTable() != null) {
            tableName = column.getTable().getName();
        }

        return (tableName != null && !tableName.isEmpty())
                ? tableName + "." + leftField
                : leftField;
    }

    /**
     * 获取右侧表达式的值
     */
    private static Object getRightValue(Expression expression) {
        if (expression instanceof StringValue) {
            return ((StringValue) expression).getValue(); // 字符串值
        } else if (expression instanceof LongValue) {
            return ((LongValue) expression).getValue(); // 整数值
        } else if (expression instanceof DoubleValue) {
            return ((DoubleValue) expression).getValue(); // 浮点数值
        } else {
            throw new IllegalArgumentException("不支持的右侧值类型: " + expression);
        }
    }

    /**
     * 统一值比较方法
     */
    private static int compareValues(Object a, Object b) {
        if (a instanceof Comparable && b instanceof Comparable && a.getClass().equals(b.getClass())) {
            @SuppressWarnings("unchecked")
            Comparable<Object> comparableA = (Comparable<Object>) a;
            return comparableA.compareTo(b);
        } else {
            throw new IllegalArgumentException("类型不匹配: 无法比较 " + a.getClass() + " 和 " + b.getClass());
        }
    }
}

这个工具类实现了完整的 SQL 表达式解析与求值逻辑,支持:

  • 基础比较运算符:=、<>、>、<、>=、<=
  • 逻辑运算符:AND、OR
  • 集合运算符:IN
  • 括号嵌套优先级处理

🔍 实战应用:从配置到执行的完整流程

规则配置示例

在实际应用中,用户可以在前台页面配置如下规则表达式:

  1. IN 表达式场景

tableA.productId IN ('4000001', '4000002')

当 TableA 的 productId 属于指定集合时触发标记

  1. 复合条件场景

(tableA.productId = '4000001' AND tableB.serverRate > 90)
OR tableA.status = 'PAID'

满足 "重要产品且高评分" 或 "已支付" 条件时触发标记

  1. 多表关联条件

tableA.createTime > '2025-06-28' AND tableB.score >= 85

TableA 创建时间在 2025-06-28 后且 TableB 评分不低于 85 时触发标记

切面编程实现

通过 Spring AOP 拦截订单更新操作,实现规则的自动触发:

java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * 订单更新切面 - 自动应用标记规则
 */
@Aspect
@Component
public class OrderMarkAspect {

    @Resource
    private RuleService ruleService;
    @Resource
    private OrderExtMapper orderExtMapper;

    /**
     * 拦截订单更新方法
     */
    @Around("execution(* com.example.mapper.orderMapper.updateOrder(..))")
    public Object handleUpdate(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        Order order = (Order) args[0];
        
        // 获取关联的TableB数据
        TableB tableB = orderExtMapper.selectById(order.getExtId());
        
        // 构建规则执行上下文
        RuleContext context = new RuleContext(order, tableB);
        
        // 加载所有标记规则
        List<Rule> rules = ruleService.loadRulesByType("mark");
        
        // 执行规则匹配
        for (Rule rule : rules) {
            if (JSqlParserUtil.evaluate(rule.getExpression(), context)) {
                // 匹配规则则应用标记
                applyMarkToEntity(order, rule.getMarkCode());
            }
        }
        
        // 执行原始更新操作
        return joinPoint.proceed();
    }

    /**
     * 标记
     */
    private void applyMarkToEntity(Order order, String markCode) {
        // 实际项目中可能需要更新数据库或其他操作
        order.addMark(markCode);
        // 记录标记日志...
    }
}

这个切面实现了非侵入式的规则触发机制,当订单更新方法被调用时,会自动:

  1. 加载相关业务数据(TableA 和 TableB)
  2. 构建规则执行上下文
  3. 匹配所有标记规则
  4. 对符合条件的订单应用标记

🧪 测试验证:规则引擎的可靠性保障

为确保规则引擎的正确性,部分测试用例如下:

测试表达式预期结果实际结果
tableA.productId = '4000001'命中
tableB.serverRate > 90命中
tableA.productId IN ('4000001', '4000003')命中
(tableA.status = 'NEW' AND tableB.score < 70) OR tableA.type = 'URGENT'命中

测试结果表明,规则引擎能够准确解析并执行各种复杂条件表达式,满足实际业务需求。

📌 总结:动态规则引擎的价值与扩展

通过 SQL 表达式与 Spring AOP 的结合,我们实现了一个轻量级但功能强大的动态规则引擎,其核心价值在于:

🌐 官方宣传版
  1. 需求响应效率提升:运营人员可直接在后台配置规则,无需开发介入
  2. 代码可维护性增强:规则配置与业务逻辑分离,避免条件判断代码膨胀
  3. 系统扩展性提高:支持无限扩展的复杂规则组合,适应业务快速变化
😎 程序员真实心声
  • 业务/产品提新需求时:
    优雅甩锅:" 自己去前台页面写 SQL 呀~"

  • 运营同学改规则时:
    边喝奶茶边提醒:" 改完记得测试!别把weekday = '周五'写成周一,不然全组陪你加班哦~"

  • 这种 "代码少改、需求照办" 的快乐,就像周五下午五点半,老板突然说:"今天不用加班,下班!"—— 爽就完事了! 🚀