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

相关推荐
程序员Sonder3 分钟前
黑马java----正则表达式(一文弄懂)
java·正则表达式·新人首发
doris82044 分钟前
Python 正则表达式 re.findall()
java·python·正则表达式
普通网友9 分钟前
PL/SQL语言的正则表达式
开发语言·后端·golang
Anastasiozzzz18 分钟前
阿亮随手记:动态条件生成Bean
java·前端·数据库
想用offer打牌19 分钟前
一站式了解火焰图的基本使用
后端·面试·架构
小王同学^ ^44 分钟前
从零开发一个操作系统(1.3) 如何使用ContextOS 智能名片打造个人IP
后端
Penge6661 小时前
Go 泛型里的 ~[]E 到底是什么
后端
树码小子1 小时前
图书管理系统(5)强制登陆(后端实现)
spring boot·mybatis·图书管理系统
丹牛Daniel1 小时前
Java解决HV000183: Unable to initialize ‘javax.el.ExpressionFactory‘
java·开发语言·spring boot·tomcat·intellij-idea·个人开发
消失的旧时光-19431 小时前
智能指针(三):实现篇 —— shared_ptr 的内部设计与引用计数机制
java·c++·c·shared_ptr