走进 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 构建等领域应用极其广泛。
📑 目录
- [模式概述 · 15 问深度分析](#模式概述 · 15 问深度分析)
- 框架源码实战分析
- [Tomcat / Servlet Filter 机制](#Tomcat / Servlet Filter 机制)
- [Spring Security FilterChain](#Spring Security FilterChain)
- [MyBatis 动态 SQL SqlNode](#MyBatis 动态 SQL SqlNode)
- [深度追问:Why / How / Trade-off / Evolution / Modern Practice](#深度追问:Why / How / Trade-off / Evolution / Modern Practice)
- 总结
- 参考文献
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: 包含哪些角色?每个角色的职责是什么?
过滤器模式通常包含以下四个核心角色:
- 目标对象/上下文(Target Object / Context) :被过滤的数据载体(如
Product类、Person类),包含具体的属性。 - 过滤器接口(Criteria / Filter Interface) :定义了统一的过滤方法,通常是
meetCriteria(List<Target>): List<Target>。 - 具体过滤器(Concrete Criteria) :实现过滤器接口,负责实现具体的、单一维度的过滤逻辑(如
CriteriaMale、CriteriaPriceRange)。 - 组合过滤器(AndCriteria / OrCriteria / NotCriteria) :同样实现过滤器接口,但内部持有其他过滤器对象,负责将多个过滤器进行逻辑上的
AND、OR或NOT组合。
Q5: 它们之间如何协作?调用流程是怎样的?
- 初始化:客户端创建一系列具体过滤器对象(A, B, C)。
- 组装 :客户端根据业务需求,将这些过滤器组装到组合过滤器中(例如:
new AndCriteria(filterA, filterB))。 - 执行:客户端将原始的数据集合传给最外层的过滤器。
- 传递 :最外层过滤器调用内部过滤器的过滤方法。
AndCriteria把第一个过滤器的输出作为第二个过滤器的输入,如同流水线一样层层递进。
Q6: 为什么要这样设计?每个角色存在的意义是什么?
- 定义接口的意义:确保所有的过滤行为都有统一的规格,使得"单一过滤器"和"组合过滤器"在外界看来没有任何区别(利用了组合模式的思想),可以被无缝替换和嵌套。
- 单一具体过滤器的意义:保证高内聚。一个类只专注做好一件事(例如只管筛选性别)。
- 组合过滤器的意义 :它是这个模式的灵魂。它不关心具体的业务筛选逻辑,只关心逻辑运算(并、交、补),从而实现了策略与逻辑控制的完全解耦。
Q7: 为什么使用接口、抽象类、组合,而不是其他方式?
- 为什么用接口/抽象类 :为了实现多态 。客户端只需要知道
Criteria接口,而不需要知道具体是哪个过滤器。 - 为什么用组合(Composition)而不是继承(Inheritance) :继承是静态的,在编译期就决定了父子关系。如果用继承来表现条件的组合,会导致类数量爆炸(例如
MaleAndAdultCriteria、MaleOrAdultCriteria)。而组合允许我们在运行时动态地把 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: 如何在现有项目中识别可以使用该模式的地方?
- 庞大的
QueryService:里面写满了诸如if (req.getAge() != null) { ... }且这些条件经常因为前端需求的改动而增删。 - 多处重复的筛选逻辑:比如在 A 报表和 B 报表中,都有一段一模一样的代码用来过滤"无效订单"。
- 配置文件或前端传回复杂的 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.doFilter 把 this(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 作为外层过滤器,不仅负责组合子节点的输出,还承担了"净化"职责------自动裁剪掉多余的 AND、OR 以及自动添加 WHERE 关键字。
三者对比总结:
| 维度 | Tomcat Servlet Filter | Spring Security FilterChain | MyBatis 动态 SQL |
|---|---|---|---|
| 过滤对象 | HTTP 请求与响应 | 安全上下文与凭证 | SQL 文本片段 |
| 核心机制 | 数组 + 下标指针遍历 | 多条链动态匹配 + 虚拟链驱动 | 树状节点的条件评估与拼接 |
| 超越传统之处 | 实现了双向拦截(U 型执行流) | 实现了动态路由(不同 URL 不同链) | 实现了结构自愈(自动修剪 AND/OR) |
3. 深度追问
3.1 Why(为什么):解决哪个根本矛盾?
过滤器模式解决的是 "无限增长的业务多维筛选需求"与"代码结构的内聚性、稳定性"之间的根本矛盾。
如果用继承拓扑会导致子类数量爆炸;如果用硬编码逻辑会导致单个方法内嵌大量 if-else,直接违反开闭原则。过滤器模式将"筛选规则"与"执行筛选的动作"彻底解耦,用运行时的对象组合 代替了编译期的类继承/硬编码逻辑。
3.2 How(怎么做):通过哪些对象关系和交互机制?
过滤器模式同时融合了策略模式与组合模式的影子:
- 同质性接口 :所有过滤器(无论是单条件过滤器还是组合过滤器)都实现同一个接口
meetCriteria(Collection<T>)。 - 多态组合 :组合过滤器(如
AndCriteria)内部持有其他过滤器的引用。 - 管道式协作:数据集合像水流一样通过管道,前一个过滤器的输出作为下一个过滤器的输入。
3.3 Trade-off(代价):牺牲了什么换取什么?
| 牺牲 | 换取 |
|---|---|
| 内存开销:每层过滤创建新的 List | 极致的灵活性:动态拼装条件 |
| CPU 周期:多次全量循环遍历 | 高度可维护性:消除 if-else 嵌套 |
| 类膨胀:每个条件一个类 | 完美复用性:规则可跨业务复用 |
3.4 Evolution(演化):如何从简单设计逐步演化而来?
- 硬编码(Monomorphic) :一个庞大的
search()方法塞满if (param != null)。 - 方法抽取(Refactoring):将判断逻辑抽取为私有方法,但未解决 OCP。
- 经典过滤器模式 :引入
Criteria接口,通过AndCriteria组合。 - 责任链化:演变为不返回新集合,而是传递 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.