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

相关推荐
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du3 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea
C澒3 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
东东5163 小时前
高校智能排课系统 (ssm+vue)
java·开发语言
余瑜鱼鱼鱼3 小时前
HashTable, HashMap, ConcurrentHashMap 之间的区别
java·开发语言
SunnyDays10113 小时前
使用 Java 自动设置 PDF 文档属性
java·pdf文档属性
鸣潮强于原神3 小时前
TSMC chip_boundary宽度规则解析
后端
我是咸鱼不闲呀4 小时前
力扣Hot100系列16(Java)——[堆]总结()
java·算法·leetcode
what丶k4 小时前
SpringBoot3 配置文件使用全解析:从基础到实战,解锁灵活配置新姿势
java·数据库·spring boot·spring·spring cloud
Code blocks4 小时前
kingbase数据库集成Postgis扩展
数据库·后端