🔥 动态标记黑科技: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 切面拦截订单更新操作,不修改原有业务逻辑
-
易维护性:规则配置与代码实现分离,运营人员可自助管理
-
轻量级:配置简单,不依赖复杂的框架或中间件,功能可快速迭代上线
整体实现思路如下:
- 用户在前端输入 SQL 风格的规则表达式
- 使用 JSqlParser 解析表达式为抽象语法树
- 通过 Spring AOP 拦截订单、订单评分表的更新方法
- 构建包含相关数据的上下文对象
- 执行规则匹配,符合条件则添加标记
📦核心组件实现:从设计到代码的完整落地
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
- 括号嵌套优先级处理
🔍 实战应用:从配置到执行的完整流程
规则配置示例
在实际应用中,用户可以在前台页面配置如下规则表达式:
-
IN 表达式场景
tableA.productId IN ('4000001', '4000002')
当 TableA 的 productId 属于指定集合时触发标记
-
复合条件场景
(tableA.productId = '4000001' AND tableB.serverRate > 90)
OR tableA.status = 'PAID'
满足 "重要产品且高评分" 或 "已支付" 条件时触发标记
-
多表关联条件
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);
// 记录标记日志...
}
}
这个切面实现了非侵入式的规则触发机制,当订单更新方法被调用时,会自动:
- 加载相关业务数据(TableA 和 TableB)
- 构建规则执行上下文
- 匹配所有标记规则
- 对符合条件的订单应用标记
🧪 测试验证:规则引擎的可靠性保障
为确保规则引擎的正确性,部分测试用例如下:
| 测试表达式 | 预期结果 | 实际结果 |
|---|---|---|
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 的结合,我们实现了一个轻量级但功能强大的动态规则引擎,其核心价值在于:
🌐 官方宣传版
- 需求响应效率提升:运营人员可直接在后台配置规则,无需开发介入
- 代码可维护性增强:规则配置与业务逻辑分离,避免条件判断代码膨胀
- 系统扩展性提高:支持无限扩展的复杂规则组合,适应业务快速变化
😎 程序员真实心声
-
业务/产品提新需求时:
优雅甩锅:" 自己去前台页面写 SQL 呀~" -
运营同学改规则时:
边喝奶茶边提醒:" 改完记得测试!别把weekday = '周五'写成周一,不然全组陪你加班哦~" -
这种 "代码少改、需求照办" 的快乐,就像周五下午五点半,老板突然说:"今天不用加班,下班!"—— 爽就完事了! 🚀