使用模板方法模式实现可扩展的动态查询过滤器

使用模板方法模式实现可扩展的动态查询过滤器

背景

在企业级应用中,列表查询页面通常需要支持多条件动态过滤。随着业务增长,不同模块的查询需求各异,但核心逻辑相似:

  • 字段白名单校验(防止 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" }
  ]
}

总结

通过模板方法模式,我们实现了:

  1. 复用:校验逻辑集中在基类
  2. 扩展:新模块只需继承并配置字段白名单
  3. 安全:强制字段白名单校验,防止 SQL 注入
  4. 灵活:钩子方法支持特殊字段处理

这种设计特别适合多模块、多表关联查询的企业级应用场景。

相关推荐
白露与泡影1 小时前
2026版Java架构师面试题及答案整理汇总
java·开发语言
历程里程碑1 小时前
滑动窗口---- 无重复字符的最长子串
java·数据结构·c++·python·算法·leetcode·django
qq_229058012 小时前
docker中检测进程的内存使用量
java·docker·容器
我真的是大笨蛋2 小时前
InnoDB行级锁解析
java·数据库·sql·mysql·性能优化·数据库开发
钦拆大仁2 小时前
Java设计模式-单例模式
java·单例模式·设计模式
小手cool2 小时前
在保持数组中对应元素(包括负数和正数)各自组内顺序不变的情况下,交换数组中对应的负数和正数元素
java
笨手笨脚の2 小时前
深入理解 Java 虚拟机-04 垃圾收集器
java·jvm·垃圾收集器·垃圾回收
skywalker_112 小时前
Java中异常
java·开发语言·异常
没有天赋那就反复3 小时前
JAVA 静态方法
java·开发语言
Java天梯之路3 小时前
Spring Boot 钩子全集实战(七):BeanFactoryPostProcessor详解
java·spring boot·后端