文档摘要
本文档详细阐述了如何利用 LiteFlow 规则引擎,将一棵由业务指标和维度组成的规则树,动态解析并拼接成可执行的 SQL 查询语句。 该方案解决了复杂、多变业务规则下数据查询的灵活性与可维护性问题。
| 项目 | 说明 |
|---|---|
| 核心框架 | LiteFlow |
| 关键技术 | 规则引擎、流程编排、SQL 生成、树结构解析 |
| 目标读者 | 后端开发工程师、架构师、数据工程师 |
1. 应用场景与核心问题
1.1 场景描述
在复杂的业务系统(如风控、营销、高级报表)中,查询条件通常不是固定的。它们源自一系列可灵活配置的业务规则,例如:
规则:
(城市 ∈ ["北京", "上海"] AND 年龄 > 30) OR (最近下单天数 < 7 AND 订单金额 > 1000)
这种规则可以被表示为一棵业务规则树,其中非叶子节点是逻辑运算符(AND/OR),叶子节点是具体的维度条件。
1.2 核心问题
- 硬编码困境:传统的
if-else或MyBatis动态标签难以维护这种嵌套深、变化频繁的逻辑。 - 可维护性差:业务逻辑与代码耦合深,规则变更需重新开发、测试、上线。
- 灵活性不足:无法支持业务人员通过界面拖拽生成新的查询规则。
1.3 解决方案
使用 LiteFlow 规则引擎,将规则树的解析和 SQL 片段的组装过程抽象为一个个可复用的组件,通过** EL 规则**进行流程编排,实现高度灵活和可维护的动态 SQL 生成。
2. 系统架构与设计理念
2.1 核心架构
+-------------------+ +-----------------------+
| 业务规则树 | | LiteFlow Context |
| (JSON/DB) | | +-------------------+ |
| - Root: OR | | | globalSqlBuilder | |--> 最终SQL
| - AND | --> | | (StringBuffer) | |
| - (维度条件A) | | | treeRootNode | |
| - (维度条件B) | | +-------------------+ |
| - AND | +-----------------------+
| - (指标条件C) | |
+-------------------+ v
+--------------+
| LiteFlow Chain|
| (EL规则编排) |
+--------------+
2.2 设计理念:组件化与编排
- 组件化:将每一种维度/指标条件的拼接逻辑封装成一个独立的 LiteFlow 节点(Node)。例如:
CityConditionCmp,AgeConditionCmp。 - 编排:规则树的遍历逻辑由 LiteFlow 的 EL 规则(
THEN,SWITCH,IF)来驱动。执行流程与业务逻辑彻底解耦。
3. 技术实现详解
3.1 数据结构定义 (规则树)
首先,需定义规则树的 JSON 结构。
{
"logicalOperator": "OR", // 非叶子节点:逻辑运算符 "AND" / "OR"
"children": [ // 子节点列表
{
"logicalOperator": "AND",
"children": [
{
"type": "dimension", // 叶子节点:维度条件
"field": "city",
"operator": "in",
"value": ["北京", "上海"]
},
{
"type": "metric", // 叶子节点:指标条件
"field": "age",
"operator": ">",
"value": 30
}
]
},
{
"logicalOperator": "AND",
"children": [
{
"type": "metric",
"field": "days_since_last_order",
"operator": "<",
"value": 7
},
{
"type": "metric",
"field": "order_amount",
"operator": ">",
"value": 1000
}
]
}
]
}
3.2 LiteFlow 组件设计
3.2.1 公共上下文 (Context)
@Data
public class SqlContext implements Context {
// 用于拼接最终SQL的容器
private StringBuffer sqlBuilder = new StringBuffer();
// 参数Map,用于PreparedStatement防SQL注入
private Map<String, Object> paramMap = new HashMap<>();
private int paramIndex = 1; // 参数索引计数器,用于生成 :param1, :param2
// 当前正在处理的规则树节点
private TreeNode currentNode;
// 添加条件的方法
public void addCondition(String conditionFragment, Object value) {
if (sqlBuilder.length() > 0) {
// 如何拼接(AND/OR)由父节点控制,这里不处理
}
String paramName = "param" + (paramIndex++);
sqlBuilder.append(" ").append(conditionFragment.replace("?", ":" + paramName));
paramMap.put(paramName, value);
}
}
3.2.2 节点组件实现 (NodeComponent)
a. 逻辑节点组件 (非叶子节点)
// 抽象逻辑节点
@Component
public abstract class LogicalNodeCmp extends NodeComponent {
@Override
public void process() {
SqlContext context = this.getContext();
TreeNode currentNode = context.getCurrentNode();
// 1. 处理当前逻辑操作符 (例如,在进入AND节点时添加 '(' )
context.getSqlBuilder().append(" ").append(getLogicalOperator()).append(" (");
// 2. 遍历子节点,并执行它们
for (TreeNode child : currentNode.getChildren()) {
context.setCurrentNode(child); // 设置当前要处理的子节点
// 根据子节点的类型,动态决定下一个要执行的组件
this.executeChild(context, child);
}
// 3. 结束当前逻辑组
context.getSqlBuilder().append(" )");
}
protected void executeChild(SqlContext context, TreeNode child) {
if ("dimension".equals(child.getType()) || "metric".equals(child.getType())) {
// 如果是叶子节点,根据 field 和 operator 路由到具体条件组件
// 例如:city -> CityConditionCmp, age -> AgeConditionCmp
String nodeId = "cond_" + child.getField() + "_" + child.getOperator();
this.execute(context, nodeId);
} else {
// 如果是非叶子节点,根据逻辑操作符路由到逻辑组件
String nodeId = "logical_" + child.getLogicalOperator().toLowerCase();
this.execute(context, nodeId);
}
}
protected abstract String getLogicalOperator();
}
// 具体实现
@Component("logical_and")
public class AndNodeCmp extends LogicalNodeCmp {
@Override protected String getLogicalOperator() { return "AND"; }
}
@Component("logical_or")
public class OrNodeCmp extends LogicalNodeCmp {
@Override protected String getLogicalOperator() { return "OR"; }
}
b. 条件节点组件 (叶子节点)
// 城市条件组件
@Component("cond_city_in")
public class CityInConditionCmp extends NodeComponent {
@Override
public void process() {
SqlContext context = this.getContext();
TreeNode node = context.getCurrentNode();
@SuppressWarnings("unchecked")
List<String> cities = (List<String>) node.getValue();
// 生成SQL片段,例如:city IN (:param1, :param2)
String placeholders = String.join(", ", Collections.nCopies(cities.size(), "?"));
context.addCondition("city IN (" + placeholders + ")", cities);
}
}
// 年龄条件组件
@Component("cond_age_>")
public class AgeGtConditionCmp extends NodeComponent {
@Override
public void process() {
SqlContext context = this.getContext();
TreeNode node = context.getCurrentNode();
context.addCondition("age > ?", node.getValue());
}
}
3.3 规则编排 (EL规则)
这是最关键的部分,但它的逻辑可以变得非常简单,因为复杂的路由逻辑已被封装在 LogicalNodeCmp 中。
<!-- 主要规则链:只需启动逻辑节点的执行 -->
<chain name="buildDynamicSqlChain">
SWITCH(decisionCmp).to(logical_and, logical_or);
</chain>
<!-- 一个决策组件,决定从根节点开始执行AND还是OR -->
@Component
public class DecisionCmp extends NodeComponent {
@Override
public String process() {
SqlContext context = this.getContext();
TreeNode rootNode = context.getCurrentNode(); // 假设根节点已被设置
return "logical_" + rootNode.getLogicalOperator().toLowerCase();
}
}
3.4 服务层调用
@Service
public class DynamicQueryService {
@Resource
private LiteFlowComponent liteFlowComponent;
public String buildDynamicSql(TreeNode ruleTreeRoot) {
// 1. 创建上下文,并设置规则树根节点
SqlContext context = new SqlContext();
context.setCurrentNode(ruleTreeRoot);
context.getSqlBuilder().append("SELECT * FROM user_table WHERE 1=1"); // 基础SQL
// 2. 执行LiteFlow规则链
LiteflowResponse response = liteFlowComponent.execute2Resp(
"buildDynamicSqlChain", null, context
);
// 3. 获取最终SQL和参数
if (response.isSuccess()) {
String finalSql = context.getSqlBuilder().toString();
Map<String, Object> params = context.getParamMap();
// 使用JdbcTemplate/MyBatis等执行 finalSql 和 params
return finalSql;
} else {
throw new RuntimeException("SQL构建失败: " + response.getMessage());
}
}
}
4. 技术难点与解决方案
| 难点 | 描述 | 解决方案 |
|---|---|---|
| 树形结构递归 | 如何优雅地遍历不确定深度的规则树。 | 利用 LiteFlow 组件自嵌套执行(this.execute())。逻辑节点处理自身逻辑后,动态决定并执行子节点,天然支持递归。 |
| 组件路由 | 如何根据动态的 field 和 operator 路由到正确的组件。 | 设计统一的组件命名规则(如 cond_{field}_{operator}),在父组件中使用 this.execute(context, nodeId) 进行动态路由。 |
| SQL注入风险 | 动态拼接SQL易引入安全漏洞。 | 绝对禁止直接拼接值。使用上下文中的 ParamMap 和 addCondition 方法统一处理,确保所有值都作为预编译参数。 |
| 括号嵌套 | 复杂的 AND/OR 逻辑需要正确的括号来保证运算优先级。 | 在逻辑节点的 process 方法中统一管理括号。在进入时加 (,在执行完所有子节点后加 )。 |
| 性能 | 深度递归和大量组件执行可能带来的开销。 | 1. LiteFlow 本身非常轻量。2. 可对解析后的SQL进行缓存(缓存key为规则树的JSON字符串或MD5)。 |
5. 总结与优势
通过将 LiteFlow 应用于动态 SQL 生成,我们实现了:
- 极致灵活:业务规则的变化只需更改规则树 JSON,无需修改代码和部署。
- 高度可维护:每个条件都是独立组件,修改、添加、复用非常简单(如添加
cond_email_like组件)。 - 清晰直观:EL 规则和组件代码共同构成了清晰的“流程图”,可读性远胜于复杂的
if-else。 - 安全可靠:统一的参数化处理机制从根本上杜绝了 SQL 注入。
- 强扩展性:未来新增函数计算(如
date_diff(...) > 7)、复杂表达式等,只需开发新组件并配置规则即可。
此方案是解决复杂动态查询需求的一个强大、优雅且面向未来的架构选择。