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

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

背景

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

  • 字段白名单校验(防止 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 天前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星1 天前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟1 天前
操作系统之虚拟内存
java·服务器·网络
Tong Z1 天前
常见的限流算法和实现原理
java·开发语言
凭君语未可1 天前
Java 中的实现类是什么
java·开发语言
He少年1 天前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新1 天前
myeclipse的pojie
java·ide·myeclipse
迷藏4941 天前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构
迷藏4941 天前
**发散创新:基于Solid协议的Web3.0去中心化身份认证系统实战解析**在Web3.
java·python·web3·去中心化·区块链
qq_433502181 天前
Codex cli 飞书文档创建进阶实用命令 + Skill 创建&使用 小白完整教程
java·前端·飞书