走进 Gang of Four 设计模式:过滤器模式

走进 Gang of Four 设计模式:过滤器模式

说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"

前置知识:Java 面向对象基础(接口、多态、组合)、Stream API 基础


关于 GoF 归属:过滤器模式(Filter Pattern)又称标准模式(Criteria Pattern),并非 GoF 《设计模式》中收录的 23 种模式之一。它融合了策略模式和组合模式的思想,广泛用于数据筛选和管道处理,常被归为补充结构型模式。


📊 设计模式分类总览

GoF 23 种设计模式可按两个维度交叉分类:类/对象 (处理方式) × 创建型/结构型/行为型(目的)。

维度 创建型(Creational) 结构型(Structural) 行为型(Behavioral)
类(Class) (通过继承复用) Factory Method 工厂方法 Adapter(类适配器) Interpreter(解释器) Template Method(模板方法)
对象(Object) (通过组合/聚合复用) Abstract Factory(抽象工厂) Builder(建造者) Prototype(原型) Singleton(单例) Adapter(对象适配器) Bridge(桥接) Composite(组合) Decorator(装饰) Facade(外观) Flyweight(享元) Proxy(代理) Chain of Resp.(责任链) Command(命令) Iterator(迭代器) Mediator(中介者) Memento(备忘录) Observer(观察者) State(状态) Strategy(策略) Visitor(访问者)

本文档聚焦的 Filter / Criteria(过滤器/标准模式) 不属于经典 GoF 23,但在数据处理、安全过滤、动态 SQL 构建等领域应用极其广泛。


📑 目录

  1. [模式概述 · 15 问深度分析](#模式概述 · 15 问深度分析)
  2. 框架源码实战分析
    • [Tomcat / Servlet Filter 机制](#Tomcat / Servlet Filter 机制)
    • [Spring Security FilterChain](#Spring Security FilterChain)
    • [MyBatis 动态 SQL SqlNode](#MyBatis 动态 SQL SqlNode)
  3. [深度追问:Why / How / Trade-off / Evolution / Modern Practice](#深度追问:Why / How / Trade-off / Evolution / Modern Practice)
  4. 总结
  5. 参考文献

1. 模式概述 · 15 问深度分析

Q1: 为什么需要这个模式?它解决了什么问题?

在实际业务中,我们经常需要对一个集合进行多维度、动态组合的筛选。例如:在一个电商平台中,用户可能需要筛选"手机"品类下"价格在 3000-5000 元之间"、"品牌为华为"、"好评度大于 95%"且"有现货"的商品。

该模式解决的核心问题是:动态组合筛选条件的复杂性 。它将"筛选规则"与"被筛选的对象集合"分离,避免了将大量繁琐的 if-else 条件判断硬编码在业务逻辑中。

Q2: 如果不用这个模式,会有什么缺陷?

如果不使用过滤器模式,通常的做法是在一个方法中写大量的 if-else 或者 switch-case 语句。

  • 代码臃肿与"散弹式"修改:当有新的筛选条件(如"促销商品")加入时,必须修改原有的处理逻辑,违反开闭原则。
  • 组合爆炸:如果需要支持"条件A 并且 条件B"或者"条件A 或者 条件C",硬编码会导致逻辑判断的组合呈指数级增长。
  • 无法复用:某个筛选逻辑(例如"判断是否过期")无法在其他业务场景中直接复用,必须复制粘贴代码。
java 复制代码
// ❌ 大量 if-else 硬编码,新增条件需要改源码
public List<Product> search(String category, Double minPrice, Double maxPrice,
                            String brand, Boolean inStock) {
    List<Product> result = products;
    if (category != null) {
        result = filterByCategory(result, category);
    }
    if (minPrice != null) {
        result = filterByMinPrice(result, minPrice);
    }
    if (brand != null) {
        result = filterByBrand(result, brand);
    }
    // 新增 "好评度" 条件需要在这里加新的 if
    return result;
}

Q3: 核心思想是什么?一句话如何概括?

  • 核心思想 :将"筛选行为"封装为独立的标准对象(Criteria),并通过组合模式将多个标准链接起来,实现对数据的层层过滤。
  • 一句话概括:把各种过滤条件变成一个个独立的"滤芯",通过自由拼接滤芯来过滤数据。

Q4: 包含哪些角色?每个角色的职责是什么?

过滤器模式通常包含以下四个核心角色:

  1. 目标对象/上下文(Target Object / Context) :被过滤的数据载体(如 Product 类、Person 类),包含具体的属性。
  2. 过滤器接口(Criteria / Filter Interface) :定义了统一的过滤方法,通常是 meetCriteria(List<Target>): List<Target>
  3. 具体过滤器(Concrete Criteria) :实现过滤器接口,负责实现具体的、单一维度的过滤逻辑(如 CriteriaMaleCriteriaPriceRange)。
  4. 组合过滤器(AndCriteria / OrCriteria / NotCriteria) :同样实现过滤器接口,但内部持有其他过滤器对象,负责将多个过滤器进行逻辑上的 ANDORNOT 组合。

Q5: 它们之间如何协作?调用流程是怎样的?

  1. 初始化:客户端创建一系列具体过滤器对象(A, B, C)。
  2. 组装 :客户端根据业务需求,将这些过滤器组装到组合过滤器中(例如:new AndCriteria(filterA, filterB))。
  3. 执行:客户端将原始的数据集合传给最外层的过滤器。
  4. 传递 :最外层过滤器调用内部过滤器的过滤方法。AndCriteria 把第一个过滤器的输出作为第二个过滤器的输入,如同流水线一样层层递进。

Q6: 为什么要这样设计?每个角色存在的意义是什么?

  • 定义接口的意义:确保所有的过滤行为都有统一的规格,使得"单一过滤器"和"组合过滤器"在外界看来没有任何区别(利用了组合模式的思想),可以被无缝替换和嵌套。
  • 单一具体过滤器的意义:保证高内聚。一个类只专注做好一件事(例如只管筛选性别)。
  • 组合过滤器的意义 :它是这个模式的灵魂。它不关心具体的业务筛选逻辑,只关心逻辑运算(并、交、补),从而实现了策略与逻辑控制的完全解耦

Q7: 为什么使用接口、抽象类、组合,而不是其他方式?

  • 为什么用接口/抽象类 :为了实现多态 。客户端只需要知道 Criteria 接口,而不需要知道具体是哪个过滤器。
  • 为什么用组合(Composition)而不是继承(Inheritance) :继承是静态的,在编译期就决定了父子关系。如果用继承来表现条件的组合,会导致类数量爆炸(例如 MaleAndAdultCriteriaMaleOrAdultCriteria)。而组合允许我们在运行时动态地把 A 和 B 拼在一起。

Q8: 这种设计符合哪些面向对象原则?

  • 开闭原则 (OCP) :当需要增加新的筛选条件时,只需编写一个新的 ColorCriteria 类,完全不需要修改现有的过滤器类或客户端代码。
  • 单一职责原则 (SRP):每个具体过滤器类只负责一项特定的过滤规则,职责非常单一。
  • 依赖倒置原则 (DIP) :客户端和组合过滤器都依赖于抽象的 Criteria 接口,而不是依赖于具体的过滤器实现。

Q9: 它有哪些优点和局限性?会带来哪些额外成本?

优点:

  • 极高的灵活性:条件可以任意动态组合。
  • 良好的复用性:过滤器类是独立的,可以在不同的业务流程中复用。
  • 代码结构清晰 :消除了复杂的嵌套 if-else

局限性与额外成本:

  • 性能开销(内存与CPU) :传统过滤器模式在每一步过滤时,通常会创建一个新的 List 来存储中间结果。如果数据量巨大或过滤器链很长,会产生大量的临时对象和内存拷贝。
  • 类数量增多:每个条件都需要一个独立的类,可能会导致类文件较多。

Q10: 它最适合哪些场景?哪些场景不应该使用?

最适合的场景:

  • 界面上有多个不确定组合的筛选框(如电商搜索页、OA 系统的员工检索)。
  • 业务规则经常变动,需要频繁增加、删除或修改筛选条件的系统。

不应该使用的场景:

  • 筛选条件非常固定,且永远不会改变。
  • 大数据量且对耗时极敏感的场景 :在内存中进行多层循环过滤效率较低,这类场景更适合在数据库端(SQL 的 WHERE 条件)或搜索引擎(Elasticsearch)端直接进行过滤。

Q11: 它与容易混淆的其他设计模式有什么区别和联系?

与策略模式(Strategy)的区别:

  • 策略模式通常是互斥 的,你只能选择"算法A"或"算法B"(如不同的支付方式)。而过滤器模式是可叠加和组合的。

与责任链模式(Chain of Responsibility)的区别:

  • 责任链模式通常包含"阻断"机制,一个节点处理了,后面的节点可能就不处理了。而过滤器模式通常会遍历完所有的过滤器,目的是为了筛选和净化集合中的数据

与装饰器模式(Decorator)的区别:

  • 装饰器是为了增强 原有对象的功能;过滤器则是为了裁剪数据集合。

Q12: 框架中有哪些典型应用?

  • Tomcat / Servlet 中的 Filter:这是最著名的变形(结合了责任链)。在请求到达 Servlet 之前,经过一系列 Filter(如编码过滤、登录校验),层层净化和校验 Request/Response。
  • Spring Security 中的 FilterChain:通过配置多个过滤器,动态决定哪些请求需要哪些安全检查。
  • MyBatis 中的动态 SQL<where>, <if>, <choose> 标签在本质上就是在逻辑层拼接和构建过滤标准。

Q13: 如何从零实现一个最小可运行版本?

java 复制代码
import java.util.ArrayList;
import java.util.List;

// 1. 目标对象
class Person {
    private String name;
    private String gender;

    public Person(String name, String gender) {
        this.name = name;
        this.gender = gender;
    }
    public String getName() { return name; }
    public String getGender() { return gender; }
    @Override
    public String toString() { return name + " (" + gender + ")"; }
}

// 2. 过滤器接口
interface Criteria {
    List<Person> meetCriteria(List<Person> persons);
}

// 3. 具体过滤器:只保留男性
class CriteriaMale implements Criteria {
    @Override
    public List<Person> meetCriteria(List<Person> persons) {
        List<Person> malePersons = new ArrayList<>();
        for (Person person : persons) {
            if (person.getGender().equalsIgnoreCase("MALE")) {
                malePersons.add(person);
            }
        }
        return malePersons;
    }
}

// 4. 组合过滤器:AND 逻辑
class AndCriteria implements Criteria {
    private Criteria criteria;
    private Criteria otherCriteria;

    public AndCriteria(Criteria criteria, Criteria otherCriteria) {
        this.criteria = criteria;
        this.otherCriteria = otherCriteria;
    }

    @Override
    public List<Person> meetCriteria(List<Person> persons) {
        List<Person> firstCriteriaPersons = criteria.meetCriteria(persons);
        return otherCriteria.meetCriteria(firstCriteriaPersons);
    }
}

// 5. 客户端调用
public class FilterPatternDemo {
    public static void main(String[] args) {
        List<Person> persons = new ArrayList<>();
        persons.add(new Person("Robert", "Male"));
        persons.add(new Person("John", "Male"));
        persons.add(new Person("Laura", "Female"));

        Criteria male = new CriteriaMale();
        List<Person> males = male.meetCriteria(persons);
        System.out.println(males); // [Robert (Male), John (Male)]
    }
}

Q14: 如何在现有项目中识别可以使用该模式的地方?

  1. 庞大的 QueryService :里面写满了诸如 if (req.getAge() != null) { ... } 且这些条件经常因为前端需求的改动而增删。
  2. 多处重复的筛选逻辑:比如在 A 报表和 B 报表中,都有一段一模一样的代码用来过滤"无效订单"。
  3. 配置文件或前端传回复杂的 JSON 逻辑树:前端需要支持自定义配置"条件1 AND (条件2 OR 条件3)",这种动态解析最适合用过滤器模式映射。

Q15: 如果重新设计这个模式,在今天会有哪些改进?

改进 1:利用 Java 8 函数式接口与 Stream API

核心接口直接复用 JDK 自带的 java.util.function.Predicate<T>。利用 Stream.filter(),不仅消除了中间多层 List 的内存拷贝开销(流式惰性求值),还天然支持并发(parallelStream())。

改进 2:结合 Lambda 表达式消灭具体类

java 复制代码
Predicate<Person> isMale = p -> "Male".equalsIgnoreCase(p.getGender());
Predicate<Person> isAdult = p -> p.getAge() > 18;

// 利用 Predicate 自带的 and / or / negate 进行组合
List<Person> result = persons.stream()
                             .filter(isMale.and(isAdult))
                             .collect(Collectors.toList());

改进 3:微服务与配置中心集成

过滤规则从配置中心(如 Nacos/Apollo)动态下发表达式字符串(如 age > 18 && gender == 'Male'),在内存中动态编译成 Predicate 执行,实现零部署的动态过滤。


2. 框架源码实战分析

2.1 Tomcat / Servlet Filter 机制

Tomcat 的 Filter 机制是过滤器模式最著名的工业变形,它融合了责任链模式(Chain of Responsibility)

核心架构与角色
  • javax.servlet.Filter(过滤器接口) :定义了 doFilter(request, response, chain) 方法。
  • javax.servlet.FilterChain(过滤器链接口) :定义了 doFilter(request, response) 方法,负责驱动下一个过滤器执行。
  • ApplicationFilterChain(Tomcat 中的具体实现) :内部维护了一个 FilterConfig[] 数组和一个自增的下标计数器 pos
核心源码与执行机制

Tomcat 没有为每个过滤器层层嵌套(如 new FilterA(new FilterB())),而是采用数组 + 下标的高效设计:

java 复制代码
public final class ApplicationFilterChain implements FilterChain {
    private FilterConfig[] filters = new FilterConfig[0];
    private int pos = 0;
    private int n = 0;
    private Servlet servlet = null;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response) {
        if (pos < n) {
            FilterConfig filterConfig = filters[pos++];
            Filter filter = filterConfig.getFilter();
            filter.doFilter(request, response, this); // 将自身作为 chain 传入
        } else {
            servlet.service(request, response); // 所有 Filter 执行完毕
        }
    }
}

妙处解析------双向拦截(U 型回溯): filter.doFilterthis(Chain 本身)传给具体 Filter,允许具体 Filter 控制请求的放行与回溯:

java 复制代码
public class LogFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        System.out.println("1. 请求进来了...");         // 前置增强
        chain.doFilter(req, res);                        // 放行
        System.out.println("2. 业务处理完了...");        // 后置增强
    }
}

这种设计让一个普通的数组结构,在运行时通过函数调用栈变成了一个 U 型的执行流(Request 顺流而下,Response 逆流而上)。

2.2 Spring Security FilterChain

Spring Security 的核心就是一个大号的过滤器链。它通过 DelegatingFilterProxy 作为桥梁,将 Servlet 容器的 Filter 机制与 Spring IoC 容器连接起来。

核心源码与执行机制

Spring Security 需要根据不同的 URL 应用不同的安全策略:

java 复制代码
public class FilterChainProxy extends GenericFilterBean {
    private List<SecurityFilterChain> filterChains;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 根据当前请求的 URL,匹配最合适的一条安全过滤器链
        List<Filter> filters = getFilters(request);

        if (filters == null || filters.isEmpty()) {
            chain.doFilter(request, response); // 没匹配到,直接放行
            return;
        }

        // 创建 Spring Security 内部的虚拟过滤器链
        VirtualFilterChain vfc = new VirtualFilterChain(chain, filters);
        vfc.doFilter(request, response);
    }
}

妙处解析------按需定制的"积木式"防御:

Spring Security 内部提供了几十个 Filter(如 UsernamePasswordAuthenticationFilter 负责表单登录,CsrfFilter 负责防跨站请求伪造)。你可以通过配置(如 .csrf().disable())在运行时把 CsrfFilter 从链上拔掉。不同的 URL 可以走完全不同的链------/public/ 走空链,/admin/ 走挂满校验的重型链。

2.3 MyBatis 动态 SQL SqlNode

在 MyBatis 中,过滤器处理的是"SQL 片段"。当写下 <if test="title != null">AND title like #{title}</if> 时,MyBatis 将其转换成一棵 SqlNode

核心架构
角色 MyBatis 对应类
过滤器接口 SqlNode(定义 apply(DynamicContext)
具体过滤器 IfSqlNode(对应 <if>)、TextSqlNode(纯文本)
组合过滤器 MixedSqlNode(持有 List<SqlNode>
增强过滤器 WhereSqlNode(自动修剪 AND/OR,添加 WHERE)
核心源码与执行机制
java 复制代码
// IfSqlNode 的核心实现
public class IfSqlNode implements SqlNode {
    private final ExpressionEvaluator evaluator;
    private final String test;              // "title != null"
    private final SqlNode contents;          // "AND title like #{title}"

    @Override
    public boolean apply(DynamicContext context) {
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            contents.apply(context); // 条件满足,拼接 SQL 片段
            return true;
        }
        return false; // 条件不满足,该片段被"过滤"掉
    }
}

妙处解析------AST 级别的过滤:

MyBatis 的动态 SQL 完美展示了过滤器模式与组合模式的结合。每个 SqlNode 根据传入的参数,决定自己这块 SQL 片段是保留还是丢弃。WhereSqlNode 作为外层过滤器,不仅负责组合子节点的输出,还承担了"净化"职责------自动裁剪掉多余的 ANDOR 以及自动添加 WHERE 关键字。

三者对比总结:

维度 Tomcat Servlet Filter Spring Security FilterChain MyBatis 动态 SQL
过滤对象 HTTP 请求与响应 安全上下文与凭证 SQL 文本片段
核心机制 数组 + 下标指针遍历 多条链动态匹配 + 虚拟链驱动 树状节点的条件评估与拼接
超越传统之处 实现了双向拦截(U 型执行流) 实现了动态路由(不同 URL 不同链) 实现了结构自愈(自动修剪 AND/OR)

3. 深度追问

3.1 Why(为什么):解决哪个根本矛盾?

过滤器模式解决的是 "无限增长的业务多维筛选需求"与"代码结构的内聚性、稳定性"之间的根本矛盾

如果用继承拓扑会导致子类数量爆炸;如果用硬编码逻辑会导致单个方法内嵌大量 if-else,直接违反开闭原则。过滤器模式将"筛选规则"与"执行筛选的动作"彻底解耦,用运行时的对象组合 代替了编译期的类继承/硬编码逻辑

3.2 How(怎么做):通过哪些对象关系和交互机制?

过滤器模式同时融合了策略模式与组合模式的影子:

  1. 同质性接口 :所有过滤器(无论是单条件过滤器还是组合过滤器)都实现同一个接口 meetCriteria(Collection<T>)
  2. 多态组合 :组合过滤器(如 AndCriteria)内部持有其他过滤器的引用。
  3. 管道式协作:数据集合像水流一样通过管道,前一个过滤器的输出作为下一个过滤器的输入。

3.3 Trade-off(代价):牺牲了什么换取什么?

牺牲 换取
内存开销:每层过滤创建新的 List 极致的灵活性:动态拼装条件
CPU 周期:多次全量循环遍历 高度可维护性:消除 if-else 嵌套
类膨胀:每个条件一个类 完美复用性:规则可跨业务复用

3.4 Evolution(演化):如何从简单设计逐步演化而来?

  1. 硬编码(Monomorphic) :一个庞大的 search() 方法塞满 if (param != null)
  2. 方法抽取(Refactoring):将判断逻辑抽取为私有方法,但未解决 OCP。
  3. 经典过滤器模式 :引入 Criteria 接口,通过 AndCriteria 组合。
  4. 责任链化:演变为不返回新集合,而是传递 Context 或拦截器链(如 Tomcat Filter)。

衍生变体:

  • 规格模式(Specification Pattern) :在领域驱动设计中广为人知,只提供 isSatisfiedBy(T item): boolean 方法。
  • 管道与过滤器(Pipe and Filter) :系统架构层面,将大型任务拆分为多个独立的标准处理步骤(如 Linux 的 ls | grep | awk)。

3.5 Modern Practice(现代实践):在今天是否被取代?

结论:经典形态(手工编写大量 Criteria 类)已被新技术取代,但其核心思想在现代实践中达到了顶峰。

函数式编程(Java 8+ Stream)的颠覆:

java 复制代码
Predicate<Person> isMale = p -> "Male".equals(p.getGender());
Predicate<Person> isAdult = p -> p.getAge() >= 18;
List<Person> result = list.stream()
    .filter(isMale.and(isAdult))
    .collect(Collectors.toList());

依赖注入(Spring IoC)与自动装配:

java 复制代码
@Service
public class OrderValidationService {
    @Autowired
    private List<OrderFilter> orderFilters;  // Spring 自动注入所有实现

    public void validate(Order order) {
        orderFilters.forEach(filter -> filter.check(order));
    }
}

微服务架构下的演变:

  • 微服务网关(Spring Cloud Gateway / Kong) :请求通过认证过滤器、限流过滤器、日志过滤器后路由到具体服务,将过滤器模式提升到应用架构级别
  • 配置中心 + 规则引擎 :过滤规则写在 Apollo/Nacos 中(如 SpEL 表达式),由动态解析器转化为 Predicate 执行,实现免部署的动态过滤。

4. 总结

核心要点

  • 过滤器模式将筛选条件封装为独立"滤芯",通过组合实现自由拼接
  • 与策略模式的区别:策略是互斥选择 ,过滤器是叠加组合
  • 三个工业级变体:Tomcat Filter(U 型回溯拦截)、Spring Security(动态路由安全链)、MyBatis SqlNode(AST 级别 SQL 过滤)
  • 现代演进:Stream + Predicate(函数式)、Spring DI 自动装配(插件式)、网关过滤器(架构级)

一句话

过滤器模式把各种过滤条件变成一个个独立的"滤芯",通过自由拼接来过滤数据------就像净水器一样,每层滤芯负责一种杂质。


5. 参考文献

1 FOWLER M. Patterns of Enterprise Application ArchitectureM. Boston: Addison-Wesley, 2002.

2 Apache Tomcat. Tomcat FilterChain Source CodeEB/OL. https://tomcat.apache.org/, 2024.

3 SPRING. Spring Security ArchitectureEB/OL. https://docs.spring.io/spring-security/, 2024.

4 MyBatis. MyBatis 3 Dynamic SQLEB/OL. https://mybatis.org/mybatis-3/dynamic-sql.html, 2024.

相关推荐
吃饱了得干活1 小时前
实时弹幕应该怎么搞?一篇带你从入门到生产级实战
java·后端
tianyuanwo2 小时前
深入掌握 java -jar 命令:从基础到 Jenkins Agent 实战
java·jenkins·jar
Full Stack Developme2 小时前
MyBatis-Plus(MP)AST 抽象语法树 设计原理
java·tomcat·mybatis
宠友信息2 小时前
消息撤回与已读状态如何在即时通讯源码中统一管理
java·spring boot·websocket·mysql·uni-app
小小放舟、2 小时前
Windows 本地安装 Elasticsearch 8.10.0 与 IK 分词器(2026)
java·大数据·windows·spring boot·elasticsearch·搜索引擎·全文检索
zzz_23682 小时前
【Java实习面试算法冲刺】总复盘
java·算法·面试
從南走到北2 小时前
JAVA无人共享系统无人场馆预约篮球系统源码支持小程序+公众号+APP+H5
java·开发语言·小程序
码智社2 小时前
Maven 零基础完整教程
java·开发语言·maven
半夜修仙2 小时前
Spring集成邮箱功能
java·开发语言·spring boot·spring·rabbitmq·github·maven