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

相关推荐
:12114 分钟前
java基础
java·开发语言
曹牧1 小时前
Spring:@RequestMapping注解,匹配的顺序与上下文无关
java·后端·spring
daixin88481 小时前
cursor无法正常使用gpt5.5等模型解决方案
java·redis·cursor
韦禾水2 小时前
记录一次项目部署到tomcat的异常
java·tomcat
曦月合一2 小时前
树莓派安装jdk、tomcat、vnc、谷歌浏览器开机自启等环境配置
java·tomcat·树莓派
阿丰资源2 小时前
SpringBoot+Vue实战:打造企业级在线文档管理系统
vue.js·spring boot·后端
Rust研习社3 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
此剑之势丶愈斩愈烈3 小时前
openssl 自建证书
java
面汤放盐3 小时前
何时使用以及何时不应使用微服务:没有银弹
java·运维·云计算
0xDevNull3 小时前
Spring Boot 自动装配:从原理到实践
java·spring boot·后端