Spring Boot Filter :doFilter 与 doFilterInternal 的差异

1. 前言

在 Spring Boot 中开发自定义 Filter 时,我们通常有两种选择:

  1. 直接实现 javax.servlet.Filter 接口,重写 doFilter 方法。
  2. 继承 Spring 提供的 OncePerRequestFilter 抽象类,重写 doFilterInternal 方法。

很多开发者会产生疑惑:这两个方法有什么本质区别?为什么 Spring 官方推荐使用后者?如果不小心选错了会有什么后果?

本文将从 Servlet 生命周期请求调度机制 以及 设计模式 三个维度,彻底讲透这两个方法的区别。


2. 核心差异:执行频率与触发机制

2.1 doFilter:Servlet 标准的守门员

来源javax.servlet.Filter 接口。

它是 Java EE (Jakarta EE) Servlet 规范中定义的标准方法。Servlet 容器(如 Tomcat)在处理请求链时,会直接调用此方法。

特性每一次请求调度(Dispatch)都会执行。

这里有一个极易被忽视的陷阱:"一次 HTTP 请求"并不等于"一次调度"。

在 Servlet 容器中,如果发生了 内部转发 (Forward),容器会再次重新触发 Filter 链。

场景复现(问题所在):

假设你写了一个 AuthFilter 用来校验 Token,并记录日志。

  1. 用户请求接口 /user/login
  2. AuthFilter.doFilter 执行(第 1 次)。
  3. 业务逻辑判断需要跳转,Servlet 内部执行了 request.getRequestDispatcher("/home").forward(request, response)
  4. Tomcat 重新分发请求到 /home
  5. AuthFilter.doFilter 再次执行(第 2 次)。

后果

  • 性能浪费:鉴权逻辑被执行了两次。
  • 数据错误:如果 Filter 里有计数器(如限流),计数会比实际多。
  • 日志冗余:访问日志被记录了两遍。

2.2 doFilterInternal:Spring 的智能扩展

来源org.springframework.web.filter.OncePerRequestFilter 抽象类。

Spring 为了解决上述"重复执行"的问题,引入了 OncePerRequestFilter

特性在一次完整的 HTTP 请求生命周期中,严格保证只执行一次。

它通过在 Request 中打"标记"的方式,识别当前请求是否已经过滤过。如果已经过滤过,直接跳过;如果没有,才调用 doFilterInternal


3. 原理剖析:模板方法模式

OncePerRequestFilter 的实现是典型的 模板方法模式(Template Method Pattern) 。它接管了标准的 doFilter,并在内部定义了执行流程骨架。

让我们通过伪代码来看懂它的内部逻辑:

java 复制代码
// Spring 源码逻辑简化版
public abstract class OncePerRequestFilter implements Filter {

    // final 禁止子类重写,确保"只执行一次"的逻辑不被破坏
    @Override
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) {
        
        // 1. 生成一个标记 Key,例如 "com.example.MyFilter.FILTERED"
        String alreadyFilteredAttributeName = this.getClass().getName() + ".FILTERED";
        
        // 2. 检查标记:是否已经执行过?
        if (request.getAttribute(alreadyFilteredAttributeName) != null) {
            // 【情况 A】已经执行过
            // 直接放行,跳过当前 Filter 的业务逻辑
            filterChain.doFilter(request, response);
        } else {
            // 【情况 B】还没执行过
            // 2.1 打上标记:防止后续转发时再次执行
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
            try {
                // 2.2 【核心】调用子类实现的业务逻辑
                doFilterInternal((HttpServletRequest) request, (HttpServletResponse) response, filterChain);
            } finally {
                // 2.3 清理标记(部分场景需要,通常请求结束就销毁了)
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }

    // 留给子类实现的抽象方法
    protected abstract void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain);
}

解析:

  • doFilter:负责控制流程,判断是否需要执行。
  • doFilterInternal:负责具体的业务逻辑(鉴权、日志、跨域设置等)。

4. 实战对比:使用便利性

除了防止重复执行,doFilterInternal 在代码编写上也更加友好。

方式一:实现原生 doFilter (不推荐)

java 复制代码
@Component
public class NativeFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        // 痛点:参数是 ServletRequest,通过 HTTP 协议获取 Header 需要强转
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String token = httpRequest.getHeader("Authorization");
        // ... 业务逻辑 ...
        
        chain.doFilter(request, response);
    }
}

方式二:继承 OncePerRequestFilter

java 复制代码
@Component
public class SpringFilter extends OncePerRequestFilter {
    
    // 优势 1:方法名明确,暗示了只执行一次的特性
    // 优势 2:参数直接是 HttpServletRequest,无需手动强转
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
            throws ServletException, IOException {
        
        String token = request.getHeader("Authorization");
        // ... 业务逻辑 ...
        
        filterChain.doFilter(request, response);
    }
}

5. 总结与决策指南

维度 doFilter doFilterInternal
所属层级 Servlet 标准 API Spring Framework 扩展
执行机制 每次请求调度都会执行 (Forward 会重复触发) 每次 HTTP 请求仅执行一次 (内置去重机制)
参数类型 ServletRequest (需强转) HttpServletRequest (开箱即用)
设计模式 普通接口实现 模板方法模式的钩子方法
使用建议 仅在非 Spring 环境或需要特定重复执行逻辑时使用 Spring Boot 项目中的默认选择

在 Spring Boot 开发中,**99% 的场景请直接继承 OncePerRequestFilter 并重写 doFilterInternal**。这不仅能避免内部转发导致的逻辑错误,还能获得更简洁的代码体验。

相关推荐
冷雨夜中漫步5 分钟前
DeepSeek Harness:一切皆插件的 AI Agent 框架深度解析
java·人工智能·ai·开源·github
IT_陈寒5 分钟前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端
qq_225891746614 分钟前
基于Flask的空气质量监测与预测分析系统
开发语言·后端·python·信息可视化·django·flask
lhldsg27 分钟前
社区健身系统开发实战:从需求分析到落地部署全流程指南
java·开发语言·小程序·需求分析
覆东流36 分钟前
4.Java运算符与表达式
java·开发语言·后端
JacksonMx1 小时前
ContentCachingRequestWrapper 实战:解决请求体“只能读一次”的官方方案
java·spring boot·spring
马丁玩编程1 小时前
GitHub 3.5k Star 之后,Ragent AI 框架新版本来了
后端·面试·github
微石科技1 小时前
医疗设备维保如何从被动维修转向预防管理?宁波微石科技让运行数据提前发信号
java·后端·struts
AINative软件工程1 小时前
LLM 应用的 Warm-Up 工程实践:冷启动延迟从 12 秒砍到 800ms 的 5 个工程手段
后端·llm·ai编程
SomeB1oody1 小时前
【RustyML入门】5.1. 回归指标
开发语言·后端·机器学习·rust·教程