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**。这不仅能避免内部转发导致的逻辑错误,还能获得更简洁的代码体验。

相关推荐
高频因子挖掘机22 分钟前
QuantDash 明明安装成功,为什么 Python 还是无法 import?
后端·github·api
Pniubi7 小时前
HashMap的扩容和树化边界
java·开发语言
Mikko77 小时前
用 Java 复刻了高中时在塞班上玩的「熔岩矿车」:纯 Swing、零依赖、没有一张图片,一个 jar 双击就能玩
java·游戏开发·swing
知了学历孙老师7 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
Java爱好狂.8 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
秋饼9 小时前
LLM API密钥动态轮换实战:Spring Vault + Spring AI + 多Provider路由
java·ai·技术分享·后端开发
马良神笔10 小时前
2026 年Rag最近技术前沿总结
后端·python·django
程序猿DD10 小时前
同样10美元,OpenDesign和Command Code的DeepSeek V4.1 Flash额度到底有多少?
后端
福兮说10 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
高频因子挖掘机10 小时前
A股量化从日线升级到5分钟线:数据量增加后最先暴露哪些工程问题?
后端·github·api