1. 前言
在 Spring Boot 中开发自定义 Filter 时,我们通常有两种选择:
- 直接实现
javax.servlet.Filter接口,重写doFilter方法。 - 继承 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,并记录日志。
- 用户请求接口
/user/login。 AuthFilter.doFilter执行(第 1 次)。- 业务逻辑判断需要跳转,Servlet 内部执行了
request.getRequestDispatcher("/home").forward(request, response)。 - Tomcat 重新分发请求到
/home。 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**。这不仅能避免内部转发导致的逻辑错误,还能获得更简洁的代码体验。