使用模板方法模式实现可扩展的动态查询过滤器
背景
在企业级应用中,列表查询页面通常需要支持多条件动态过滤。随着业务增长,不同模块的查询需求各异,但核心逻辑相似:
- 字段白名单校验(防止 SQL 注入)
- 操作符校验(等于、包含、大于、日期范围等)
- 排序字段校验
- 分页参数校验
如果每个模块都写一套校验逻辑,会导致大量重复代码,维护成本高。
解决方案
采用模板方法模式,将通用的校验流程抽象到基类,子类只需提供差异化的配置。
设计模式应用
1. 模板方法模式 (Template Method Pattern)
核心思想:在抽象类中定义算法骨架,将某些步骤延迟到子类实现。
java
public abstract class BaseQueryUtils {
// ========== 模板方法:定义校验流程骨架 ==========
public void validateFilters(List<? extends FilterCondition> filters) {
if (filters == null || filters.isEmpty()) {
return;
}
// 1. 获取字段白名单(抽象方法,子类必须实现)
Map<String, String> allowedFields = getAllowedFields();
for (FilterCondition filter : filters) {
// 2. 校验字段是否在白名单中
String fieldName = filter.getField();
if (!allowedFields.containsKey(fieldName)) {
throw new BusinessException("非法的过滤字段: " + fieldName);
}
// 3. 自动填充表别名
filter.setAlias(allowedFields.get(fieldName));
// 4. 处理特殊字段(钩子方法,子类可选覆盖)
processSpecialField(filter);
// 5. 校验操作符
validateOperator(filter);
}
}
// ========== 抽象方法:子类必须实现 ==========
protected abstract Map<String, String> getAllowedFields();
// ========== 钩子方法:子类可选覆盖 ==========
protected void processSpecialField(FilterCondition filter) {
// 默认空实现
}
protected Set<String> getAllowedSortFields() {
return Collections.emptySet();
}
// ========== 通用方法:所有子类共享 ==========
protected void validateOperator(FilterCondition filter) {
String operator = filter.getOperator();
String dataType = filter.getDataType();
Set<String> allowedOperators = switch (dataType) {
case "number" -> Set.of("eq", "ne", "gt", "gte", "lt", "lte");
case "date" -> Set.of("dateEq", "dateGt", "dateGte", "dateLt", "dateLte");
case "select" -> Set.of("eq", "ne");
default -> Set.of("eq", "ne", "contains", "startsWith", "endsWith");
};
if (!allowedOperators.contains(operator)) {
throw new BusinessException("非法的操作符: " + operator);
}
}
}
子类实现:只需关注差异化配置
java
public class ProductQueryUtils extends BaseQueryUtils {
private static final ProductQueryUtils INSTANCE = new ProductQueryUtils();
// 字段白名单:key=前端字段名,value=表别名
private static final Map<String, String> ALLOWED_FIELDS = Map.ofEntries(
// 主表字段
Map.entry("product_name", "p"),
Map.entry("price", "p"),
Map.entry("status", "p"),
Map.entry("create_time", "p"),
// 关联表字段
Map.entry("category_name", "c"),
Map.entry("supplier_name", "s")
);
@Override
protected Map<String, String> getAllowedFields() {
return ALLOWED_FIELDS;
}
@Override
protected void processSpecialField(FilterCondition filter) {
// 特殊处理:前端传 supplier_name,实际查询 name 字段
if ("supplier_name".equals(filter.getField())) {
filter.setField("name");
}
}
@Override
protected Set<String> getAllowedSortFields() {
return Set.of("create_time", "price", "product_name");
}
public static ProductQueryUtils getInstance() {
return INSTANCE;
}
}
2. 单例模式 (Singleton Pattern)
工具类无状态,使用饿汉式单例避免重复创建:
java
public class ProductQueryUtils extends BaseQueryUtils {
private static final ProductQueryUtils INSTANCE = new ProductQueryUtils();
private ProductQueryUtils() { } // 私有构造
public static ProductQueryUtils getInstance() {
return INSTANCE;
}
// 静态便捷方法
public static void validateAndProcess(QueryParams params) {
INSTANCE.validateFilters(params.getFilters());
}
}
3. 策略模式 (Strategy Pattern) - 隐式体现
通过 FilterCondition 接口定义统一契约,不同查询参数类实现该接口:
java
// 基类中定义接口
public interface FilterCondition {
String getField();
void setField(String field);
String getOperator();
Object getValue();
String getDataType();
String getAlias();
void setAlias(String alias);
}
// 各模块的查询参数类实现该接口
public class ProductQueryParams {
private List<FilterCondition> filters;
@Data
public static class FilterCondition implements BaseQueryUtils.FilterCondition {
private String field;
private String operator;
private Object value;
private String dataType;
private String alias;
}
}
MyBatis 动态 SQL 配合
xml
<select id="getProductPage" resultType="ProductVO">
SELECT p.*, c.name as categoryName, s.name as supplierName
FROM product p
LEFT JOIN category c ON p.category_id = c.id
LEFT JOIN supplier s ON p.supplier_id = s.id
<where>
p.is_deleted = 0
<!-- 动态过滤条件 -->
<if test="params.filters != null and params.filters.size() > 0">
<foreach collection="params.filters" item="filter">
<choose>
<when test="filter.operator == 'eq'">
AND ${filter.alias}.${filter.field} = #{filter.value}
</when>
<when test="filter.operator == 'contains'">
AND ${filter.alias}.${filter.field} LIKE CONCAT('%', #{filter.value}, '%')
</when>
<when test="filter.operator == 'gt'">
AND ${filter.alias}.${filter.field} > #{filter.value}
</when>
<when test="filter.operator == 'dateGte'">
AND DATE(${filter.alias}.${filter.field}) >= DATE(#{filter.value})
</when>
<!-- 更多操作符... -->
</choose>
</foreach>
</if>
</where>
ORDER BY p.${params.sortBy} ${params.sortOrder}
</select>
类图
┌─────────────────────────────────────┐
│ BaseQueryUtils (抽象类) │
├─────────────────────────────────────┤
│ + validateFilters() 模板方法 │
│ + validateSortField() 通用方法 │
│ + validateOperator() 通用方法 │
├─────────────────────────────────────┤
│ # getAllowedFields() 抽象方法 │
│ # processSpecialField() 钩子方法 │
│ # getAllowedSortFields() 钩子方法 │
└──────────────┬──────────────────────┘
│ 继承
┌──────────┴──────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ProductQuery │ │ OrderQuery │
│ Utils │ │ Utils │
├─────────────┤ ├─────────────┤
│ALLOWED_FIELDS│ │ALLOWED_FIELDS│
│SORT_FIELDS │ │SORT_FIELDS │
└─────────────┘ └─────────────┘
优势
| 方面 | 传统方式 | 模板方法模式 |
|---|---|---|
| 代码复用 | 每个模块重复写校验逻辑 | 通用逻辑写一次 |
| 扩展性 | 新增模块需复制粘贴 | 继承基类,实现抽象方法 |
| 维护性 | 修改需改多处 | 修改基类即可 |
| 安全性 | 容易遗漏校验 | 强制实现白名单 |
前端调用示例
json
POST /api/v1/product/page
{
"pageNum": 1,
"pageSize": 20,
"sortBy": "create_time",
"sortOrder": "DESC",
"filters": [
{ "field": "product_name", "operator": "contains", "value": "手机", "dataType": "string" },
{ "field": "price", "operator": "gte", "value": 1000, "dataType": "number" },
{ "field": "status", "operator": "eq", "value": 1, "dataType": "select" },
{ "field": "create_time", "operator": "dateGte", "value": "2025-01-01", "dataType": "date" }
]
}
总结
通过模板方法模式,我们实现了:
- 复用:校验逻辑集中在基类
- 扩展:新模块只需继承并配置字段白名单
- 安全:强制字段白名单校验,防止 SQL 注入
- 灵活:钩子方法支持特殊字段处理
这种设计特别适合多模块、多表关联查询的企业级应用场景。