过滤器模式:从接口到链,再到实战
过滤器模式,说白了就是一条处理流水线。每个过滤器只做一件事,做完传给下一个。我最早接触这个模式是在做Java Web项目的时候,那时候被各种拦截逻辑搞得焦头烂额,直到理解了过滤器链的设计思想,才真正体会到什么叫「解耦」。
过滤器模式的核心角色
先看三个核心角色,这是我个人习惯的拆解方式:
- 过滤器接口(Filter):定义统一的处理入口
- 过滤器链(FilterChain):管理过滤器的执行顺序
- 上下文(Context):在过滤器之间传递数据
你想想看,如果没有过滤器链,你要怎么处理登录校验、日志记录、权限检查这些逻辑?全塞在一个方法里?那代码就成了一锅粥。
先看一个简单的Java实现
我习惯从最基础的接口开始写起:
// 过滤器接口
public interface Filter {
void doFilter(Request request, Response response, FilterChain chain);
}
// 过滤器链
public class FilterChain {
private List<Filter> filters = new ArrayList<>();
private int index = 0;
public FilterChain addFilter(Filter filter) {
filters.add(filter);
return this;
}
public void doFilter(Request request, Response response) {
if (index < filters.size()) {
Filter filter = filters.get(index);
index++;
filter.doFilter(request, response, this);
}
}
}
// 上下文
public class RequestContext {
private Map<String, Object> attributes = new HashMap<>();
public void setAttribute(String key, Object value) {
attributes.put(key, value);
}
public Object getAttribute(String key) {
return attributes.get(key);
}
}
嗯,这里要注意:FilterChain 里的 index 是线程不安全的。我在项目中遇到过并发请求导致过滤器执行顺序错乱的问题,后来改成了每个请求创建独立的 FilterChain 实例。
过滤器链的执行流程
我画了一张图,帮你理解整个调用过程:
每个过滤器处理完自己的逻辑后,调用 chain.doFilter() 把控制权交给下一个。如果某个过滤器决定中断(比如认证失败),它就不调用 chain.doFilter(),请求就停在这里了。
Java Servlet Filter 与 Spring Filter 的对比
这两个框架的过滤器实现,我都有过实战经验。直接上对比表:
| 特性 | Java Servlet Filter | Spring Filter |
|---|---|---|
| 接口定义 | javax.servlet.Filter |
javax.servlet.Filter(底层相同) |
| 配置方式 | web.xml 或 @WebFilter | @Component + @Order,或 Java Config |
| 依赖注入 | 不支持(需要手动获取) | 支持(Spring 容器管理) |
| 执行顺序 | 按 web.xml 中的声明顺序 | 按 @Order 注解或 Ordered 接口 |
| 拦截粒度 | URL 路径匹配 | URL 路径 + 注解 + 自定义匹配器 |
Spring Filter 的实战写法
这是我常用的 Spring Filter 写法:
@Component
@Order(1)
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
long startTime = System.currentTimeMillis();
System.out.println("请求开始: " + httpRequest.getRequestURI());
chain.doFilter(request, response);
long duration = System.currentTimeMillis() - startTime;
System.out.println("请求结束, 耗时: " + duration + "ms");
}
}
我曾经犯过一个低级错误:在 doFilter 方法里忘记调用 chain.doFilter(),结果所有请求都卡在日志过滤器这里,页面一直转圈。排查了半天才发现是少了一行代码。
HttpServletRequest 或 HttpServletResponse,记得用包装类(Wrapper)而不是直接修改原始对象。我曾经直接修改了 response 的字符编码,结果导致后续 Filter 拿到的是错误编码的数据。
过滤器链的变体:责任链模式
过滤器模式其实是责任链模式的一种特例。两者的区别在于:
- 过滤器链:每个过滤器都会执行,除非主动中断
- 责任链:找到一个能处理的处理器就停止,不再继续
我一般在做中间件开发时用过滤器链,比如请求日志、参数校验、数据脱敏这些「每个请求都要走一遍」的逻辑。而责任链更适合做审批流程、异常处理这类「找到第一个能处理的就结束」的场景。
上下文的设计要点
上下文(Context)是过滤器之间传递数据的桥梁。我总结了几条设计原则:
- 线程安全:每个请求要有独立的上下文实例
- 轻量级:不要往上下文里塞太多东西,否则内存压力大
- 只读 vs 可变:明确哪些属性是只读的,哪些可以修改
ThreadLocal 来存储上下文,这样每个线程(每个请求)自动拥有独立的上下文实例。但要注意,请求结束后一定要清理 ThreadLocal,否则会造成内存泄漏。
总结一下
过滤器模式的核心价值在于:把横切关注点(日志、认证、权限、监控)从业务逻辑中抽离出来。每个过滤器只关心自己的事,通过链式调用组合起来。
我在实际项目中,通常会把过滤器分为三类:
- 前置过滤器:请求进来时执行(日志、认证、参数校验)
- 后置过滤器:响应出去时执行(数据脱敏、响应压缩)
- 环绕过滤器:前后都执行(性能监控、事务管理)
嗯,过滤器模式看起来简单,但用好了能让代码结构清晰很多。你可以在自己的项目里试试,把那些散落在各个 Controller 里的重复逻辑抽成过滤器,效果立竿见影。