过滤器模式:从接口到链,再到实战

过滤器模式,说白了就是一条处理流水线。每个过滤器只做一件事,做完传给下一个。我最早接触这个模式是在做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 实例。

过滤器链的执行流程

我画了一张图,帮你理解整个调用过程:

过滤器链执行流程 请求 Filter 1: 日志 记录请求时间 Filter 2: 认证 校验Token Filter 3: 权限 检查角色 业务处理

每个过滤器处理完自己的逻辑后,调用 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 本质上还是 Servlet Filter,但 Spring 提供了更优雅的封装。你可以直接在 Filter 里注入 Service、Repository,不用像原生 Servlet Filter 那样手动从 ApplicationContext 里拿。

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(),结果所有请求都卡在日志过滤器这里,页面一直转圈。排查了半天才发现是少了一行代码。

避坑指南:如果你在 Filter 中修改了 HttpServletRequestHttpServletResponse,记得用包装类(Wrapper)而不是直接修改原始对象。我曾经直接修改了 response 的字符编码,结果导致后续 Filter 拿到的是错误编码的数据。

过滤器链的变体:责任链模式

过滤器模式其实是责任链模式的一种特例。两者的区别在于:

  • 过滤器链:每个过滤器都会执行,除非主动中断
  • 责任链:找到一个能处理的处理器就停止,不再继续

我一般在做中间件开发时用过滤器链,比如请求日志、参数校验、数据脱敏这些「每个请求都要走一遍」的逻辑。而责任链更适合做审批流程、异常处理这类「找到第一个能处理的就结束」的场景。

上下文的设计要点

上下文(Context)是过滤器之间传递数据的桥梁。我总结了几条设计原则:

  1. 线程安全:每个请求要有独立的上下文实例
  2. 轻量级:不要往上下文里塞太多东西,否则内存压力大
  3. 只读 vs 可变:明确哪些属性是只读的,哪些可以修改
我的习惯:ThreadLocal 来存储上下文,这样每个线程(每个请求)自动拥有独立的上下文实例。但要注意,请求结束后一定要清理 ThreadLocal,否则会造成内存泄漏。

总结一下

过滤器模式的核心价值在于:把横切关注点(日志、认证、权限、监控)从业务逻辑中抽离出来。每个过滤器只关心自己的事,通过链式调用组合起来。

我在实际项目中,通常会把过滤器分为三类:

  • 前置过滤器:请求进来时执行(日志、认证、参数校验)
  • 后置过滤器:响应出去时执行(数据脱敏、响应压缩)
  • 环绕过滤器:前后都执行(性能监控、事务管理)

嗯,过滤器模式看起来简单,但用好了能让代码结构清晰很多。你可以在自己的项目里试试,把那些散落在各个 Controller 里的重复逻辑抽成过滤器,效果立竿见影。

公众号:蓝海资料掘金营,微信deep3321