Spring Boot拦截器与过滤器:区别、实现与实战指南
- 发布时间:2026-05-07 06:02:16
- 本文热度:浏览 19 赞 0 评论 0
- 文章标签: Spring Boot Interceptor Filter
- 全文共1字,阅读约需1分钟
为什么 Filter 和 Interceptor 容易被混淆
在 Spring Boot 项目里,二者都能“拦请求”,都能做日志、鉴权、审计、限流前置处理,所以很多人把它们当成同一类东西使用。真正的区别不在“能不能拦”,而在拦截位置、依赖体系、可见上下文、适用目标。
一句话先定性:
Filter是 Servlet 规范层 的组件,工作在整个 Web 请求链更靠前的位置Interceptor是 Spring MVC 层 的组件,工作在DispatcherServlet分发之后、Controller 执行之前后
这意味着它们解决的问题并不完全相同。
先看执行链路
一次典型 HTTP 请求在 Spring Boot MVC 应用中的简化流程如下:
客户端请求
↓
Servlet Container
↓
Filter 链
↓
DispatcherServlet
↓
HandlerMapping
↓
Interceptor.preHandle()
↓
Controller
↓
Interceptor.postHandle()
↓
视图渲染 / 返回响应体
↓
Interceptor.afterCompletion()
↓
Filter 返回
↓
响应客户端
如果是异常场景,afterCompletion() 仍然会执行;如果是异步请求,执行时机还会更复杂,这也是很多线上问题的来源之一。
核心区别:不是谁更高级,而是谁离请求更近
| 对比项 | Filter | Interceptor |
|---|---|---|
| 所属体系 | Servlet 规范 | Spring MVC |
| 生效位置 | DispatcherServlet 之前 |
DispatcherServlet 之后 |
| 作用目标 | 进入当前 Servlet 链的请求 | 进入 Spring MVC 处理链的请求 |
| 是否依赖 Spring MVC | 不依赖 | 依赖 |
| 典型接口 | Filter / OncePerRequestFilter |
HandlerInterceptor |
| 可拿到 Controller 信息 | 拿不到 | 能拿到 handler,可判断具体处理器 |
| 是否适合改写 Request/Response | 非常适合 | 不适合做底层包装 |
| 是否适合统一编码、跨域、请求体包装 | 适合 | 不适合 |
| 是否适合基于业务接口的权限控制 | 一般 | 更适合 |
| 顺序控制方式 | Filter 注册顺序 / @Order / FilterRegistrationBean |
addInterceptors 注册顺序 |
| 与 Spring 异常体系集成 | 弱 | 强 |
| 适合范围 | 更底层、更通用 | 更贴近业务路由 |
最容易记住的判断方式是:
- 凡是跟 Servlet 请求/响应对象本身强相关的,优先考虑
Filter - 凡是跟 Controller、接口权限、业务上下文强相关的,优先考虑
Interceptor
Filter 深入解析
Filter 的本质
Filter 来自 Servlet 规范,不是 Spring MVC 发明的。它在请求进入 DispatcherServlet 之前执行,所以它更像一个 Web 基础设施组件。
常见场景:
- 统一字符编码
- CORS 处理
- 请求日志打点
traceId注入- 请求体包装
- XSS 过滤
- GZIP 压缩
- 黑名单 IP 拦截
- 对请求和响应做底层增强
Spring Boot 中虽然可以直接实现 jakarta.servlet.Filter,但更推荐继承 OncePerRequestFilter,因为它能避免同一次请求在某些派发场景下被重复执行。
Spring Boot 3.x 中实现一个 Filter
以下示例基于 Spring Boot 3.x / Spring Framework 6.x 该版本下 Servlet API 包名为
jakarta.servlet.*,不是javax.servlet.*
package com.example.demo.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdFilter extends OncePerRequestFilter {
public static final String TRACE_ID = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
request.setAttribute(TRACE_ID, traceId);
response.setHeader("X-Trace-Id", traceId);
MDC.put(TRACE_ID, traceId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}
这个 Filter 做了三件事:
- 从请求头读取
X-Trace-Id - 如果没有就生成一个
- 放入
request、响应头和日志上下文MDC
这种事情用 Interceptor 也能做,但在请求链更前面做更合理,因为后续所有组件都可能需要它。
精确控制 Filter 的生效范围和顺序
如果你不想让 Filter 拦所有请求,或者要控制多个 Filter 顺序,使用 FilterRegistrationBean。
package com.example.demo.config;
import com.example.demo.filter.TraceIdFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TraceIdFilter> traceIdFilterRegistration(TraceIdFilter traceIdFilter) {
FilterRegistrationBean<TraceIdFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(traceIdFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
registrationBean.setName("traceIdFilter");
return registrationBean;
}
}
这里需要注意两点:
order越小,越早执行urlPatterns是 Servlet 风格匹配,不是 Spring MVC 的 Ant 路径匹配
Filter 更适合处理“请求对象本身”
比如你想记录请求体,但 HttpServletRequest 输入流默认只能读一次。这类问题必须在更底层解决,通常通过包装请求对象实现。
package com.example.demo.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class RequestResponseLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
long start = System.currentTimeMillis();
try {
filterChain.doFilter(requestWrapper, responseWrapper);
} finally {
long cost = System.currentTimeMillis() - start;
String requestBody = new String(
requestWrapper.getContentAsByteArray(),
StandardCharsets.UTF_8
);
String responseBody = new String(
responseWrapper.getContentAsByteArray(),
StandardCharsets.UTF_8
);
System.out.println("URI=" + request.getRequestURI()
+ ", cost=" + cost + "ms"
+ ", req=" + requestBody
+ ", resp=" + responseBody);
responseWrapper.copyBodyToResponse();
}
}
}
这里最后一行 copyBodyToResponse() 不能漏。漏掉后,客户端可能收不到响应体。这是一个非常常见的坑。
Interceptor 深入解析
Interceptor 的本质
Interceptor 属于 Spring MVC,请求已经进入 DispatcherServlet,并且已经找到了候选处理器,此时它才有机会参与。
它最关键的价值不是“也能拦请求”,而是它能感知 Spring MVC 语义:
- 当前匹配的是哪个 Handler
- 能不能放行到 Controller
- Controller 执行完之后是否还要处理
- 请求结束后是否做资源清理
- 能和 Spring MVC 的参数绑定、异常处理、返回值处理逻辑协同
因此它特别适合做:
- 登录态校验
- 基于接口粒度的权限控制
- 幂等校验
- 接口访问审计
- 针对特定 Controller 的埋点
- 接口级别限流前置校验
- 国际化、租户、用户上下文注入
HandlerInterceptor 的三个核心方法
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
}
}
preHandle
在 Controller 执行之前调用。
返回值含义非常直接:
true:放行,继续执行后续拦截器和 Controllerfalse:中断请求链,不再进入 Controller
postHandle
Controller 正常执行完成后、视图渲染之前调用。
适合:
- 给
ModelAndView补充公共数据 - 补充响应级日志信息
对于 @ResponseBody / REST 接口,它的使用价值通常没那么大。
afterCompletion
请求结束后调用,常用于:
- 资源释放
- 清理
ThreadLocal - 记录最终异常信息
- 记录完整耗时
Spring Boot 中实现一个 Interceptor
下面做一个最常见的登录校验示例。
package com.example.demo.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null || token.isBlank()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("""
{"code":401,"message":"未登录或 token 缺失"}
""");
return false;
}
return true;
}
}
然后注册到 MVC 配置中:
package com.example.demo.config;
import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
public WebMvcConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/auth/login",
"/api/auth/register",
"/error"
);
}
}
这个配置体现了 Interceptor 最有价值的能力:按业务接口路径精确控制拦截范围。
handler 参数为什么重要
很多时候,Interceptor 比 Filter 更适合做业务鉴权,不是因为它“更 Spring”,而是因为它能拿到 handler。
if (handler instanceof HandlerMethod handlerMethod) {
Class<?> beanType = handlerMethod.getBeanType();
String methodName = handlerMethod.getMethod().getName();
System.out.println("当前请求命中 Controller: " + beanType.getName() + "#" + methodName);
}
这意味着你可以做到:
- 只拦带某个注解的方法
- 不同 Controller 使用不同鉴权策略
- 记录接口维度审计日志
- 识别匿名接口与内部接口
这类能力在 Filter 中天然做不到。
一个容易被说错的点:Interceptor 并不等于“只拦 Controller”
更准确的说法是:
Filter作用于 Servlet 请求链Interceptor作用于 Spring MVC 的HandlerExecutionChain
只要请求进入 Spring MVC 并且匹配到某个 Handler,就可能被 Interceptor 处理。 在 Spring Boot 默认静态资源配置下,一部分静态资源请求也是由 Spring MVC 的资源处理器接管的,因此Interceptor 是否会处理静态资源,取决于资源是否经过 Spring MVC Handler 链以及你的路径配置。
所以不要简单记成“Filter 拦所有,Interceptor 只拦 Controller”,这个说法不够准确。
实战建议:日志、鉴权、上下文注入应该怎么分层
项目里最常见的问题不是不会写,而是职责放错层。
一个稳定的分层方式通常是:
放到 Filter 的事情
- 生成或透传
traceId - CORS
- 请求/响应包装
- 原始请求日志
- 编码处理
- IP 黑白名单
- 基础安全头写入
放到 Interceptor 的事情
- 用户登录态校验
- RBAC 权限判断
- 接口幂等校验
- 多租户上下文注入
- 接口级审计
- 基于注解的业务规则拦截
这套分层的本质是:
- Filter 解决“请求进入系统时”的基础设施问题
- Interceptor 解决“请求进入具体业务接口前后”的业务治理问题
组合实战:Filter 负责 traceId,Interceptor 负责登录校验
这是企业项目里非常常见、也非常合理的组合。
第一步:Filter 写入 traceId
前面已经有 TraceIdFilter,它负责把追踪信息放入请求上下文。
第二步:Interceptor 读取 traceId 并做鉴权
package com.example.demo.interceptor;
import com.example.demo.filter.TraceIdFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String traceId = (String) request.getAttribute(TraceIdFilter.TRACE_ID);
String token = request.getHeader("Authorization");
if (token == null || token.isBlank()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("""
{"code":401,"message":"未登录","traceId":"%s"}
""".formatted(traceId));
return false;
}
return true;
}
}
这样做的好处是:
- 无论请求是否进入 Controller,都已经有
traceId - 拦截器返回的鉴权失败响应也能带上统一链路标识
- 日志、异常、审计都能串起来
这比把所有事情一股脑塞进一个 Filter 或一个 Interceptor 更清晰。
什么时候优先选 Filter
下面这些情况,优先用 Filter 基本不会错:
1. 需要在 Spring MVC 之前执行
比如你要做跨域、统一编码、最早期请求日志,这些都应该尽可能靠前。
2. 需要包装 HttpServletRequest / HttpServletResponse
例如缓存请求体、重复读取响应体、统一输出压缩、修改 Header。
3. 处理对象与具体业务无关
比如 traceId、底层审计、性能打点、网关前置规则。
4. 希望能力更通用
Filter 属于 Servlet 标准,不依赖 Spring MVC,迁移到别的 Servlet 应用依然成立。
什么时候优先选 Interceptor
下面这些情况,更适合 Interceptor:
1. 需要知道当前命中的是哪个接口
权限控制、注解识别、接口级审计,都依赖 handler。
2. 逻辑明显属于业务接口层
例如登录校验、租户识别、幂等校验、接口白名单。
3. 需要与 Spring MVC 行为保持一致
包括路径匹配、异常链、控制器生命周期。
4. 希望只对某些接口生效
addPathPatterns 和 excludePathPatterns 在业务场景下通常比 Filter 的 URL 模式更顺手。
常见误区与线上坑点
误区一:把所有鉴权都写进 Filter
这会导致两个问题:
- 你拿不到明确的 Controller 方法信息
- 很容易把业务规则和基础设施规则混在一起
基础接入控制可以在 Filter 做,比如非法来源直接拒绝。 但接口级权限、注解式权限校验,通常放在 Interceptor 更合适。
误区二:在 Interceptor 里做请求体读取
Interceptor 能拿到 HttpServletRequest,但它不适合承担底层请求包装职责。 如果你在这里直接读取输入流,很容易影响后续参数绑定,导致 Controller 拿不到请求体。
涉及请求体缓存、重复读取、响应体包装,应该在 Filter 层处理。
误区三:忘记异步请求的特殊性
如果 Controller 返回的是异步结果,比如 Callable、DeferredResult,请求线程和业务执行线程可能不是同一个。
这时要注意:
ThreadLocal里的上下文可能丢失postHandle的触发时机和你想象的不一样afterCompletion才更适合做最终清理
如果你在拦截器里保存用户上下文到 ThreadLocal,一定要在 afterCompletion 中清理。
误区四:多个拦截器/过滤器顺序没设计
系统复杂以后,通常会同时存在:
- 日志 Filter
- 安全头 Filter
- traceId Filter
- 登录拦截器
- 权限拦截器
- 幂等拦截器
如果顺序不清晰,常见结果是:
- 未登录日志没有 traceId
- 幂等校验早于登录校验
- 异常处理和日志落库缺字段
建议明确一套顺序策略:
Filter 顺序建议
- traceId
- 请求包装 / 请求日志
- CORS / 安全头
- 其他基础设施 Filter
Interceptor 顺序建议
- 登录校验
- 权限校验
- 幂等校验
- 审计增强
误区五:preHandle(false) 后什么都不写
如果你返回 false,Spring MVC 不会继续进入 Controller。 但这不等于客户端就自动得到一个规范响应。你必须自己明确设置:
- HTTP 状态码
- 响应头
- 响应体
否则很容易出现前端收到空响应、乱码或者难以解析的错误。
Spring Boot 2.x 与 3.x 这里最容易踩的版本差异
如果你在搜索资料时看到很多旧代码,最常见的版本坑是 Servlet 包名变化。
Spring Boot 2.x
使用:
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
Spring Boot 3.x
使用:
import jakarta.servlet.Filter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
如果你是 Boot 3 项目,却复制了 Boot 2 的 javax.servlet.* 示例代码,编译会直接失败。
这不是拦截器或过滤器原理的问题,而是 Jakarta EE 命名空间升级带来的版本差异。
一个实用的选择口诀
遇到需求时,可以直接用下面这组判断:
需求是“改请求、包响应、做底层链路”
选 Filter
需求是“看接口、做鉴权、按业务规则拦截”
选 Interceptor
两者都需要
组合使用,职责分层,不要互相替代
总结
Filter 和 Interceptor 不是谁取代谁的关系,而是位于不同层次的两个拦截点。
Filter 更底层,属于 Servlet 规范,适合处理请求对象本身和全局基础设施问题。 Interceptor 更贴近 Spring MVC,适合处理接口级别的业务治理问题。
在 Spring Boot 项目里,最稳妥的实践不是二选一,而是分工明确:
- 用
Filter处理链路追踪、请求包装、跨域、底层日志 - 用
Interceptor处理登录校验、权限控制、租户上下文、业务审计
真正写得好的项目,不是“哪里都能拦”,而是“每一层只做该做的事”。