两者不在同一层
"登录校验到底该写在 Filter 还是 Interceptor?"这个问题在面试和代码评审里都很常见。不少人觉得两者差不多,都是"请求进来先执行一段代码",于是随手选一个,后面遇到拿不到 Handler、注入不了 Bean、跨域预检被拦之类的问题,才发现选错了层。
简单说:Filter 属于 Servlet 规范,工作在 DispatcherServlet 之前;Interceptor 属于 Spring MVC,工作在 DispatcherServlet 内部、Handler 已经确定之后。层级不同,能拿到的东西和能做的事就不同。
一条请求经过谁
一个普通的 HTTP 请求进入 Tomcat 后,大致经过这些环节:
- 连接器接收请求,交给 FilterChain;
- 各个 Filter 依次执行
doFilter; - 进入
DispatcherServlet,由HandlerMapping找到对应的 Controller 方法; - 执行
HandlerInterceptor.preHandle; - 通过
HandlerAdapter调用 Controller; - 执行
postHandle,此时 Controller 已经返回,视图或响应体还没真正写出; - 执行
afterCompletion; - 回到 Filter 链,执行
chain.doFilter之后的代码。
顺序记成一句话:Filter 包着 DispatcherServlet,Interceptor 包着 Controller。所以 Filter 能改 HttpServletRequest、HttpServletResponse 本身,Interceptor 能拿到 HandlerMethod,知道"这次请求到底要进哪个方法"。
选择标准
| 需求 | 推荐 | 原因 |
|---|---|---|
| 统一编码、请求体包装 | Filter | 需要在最外层替换 request |
| traceId 写入 MDC | Filter | 要覆盖整个请求,包括 MVC 之外的异常 |
| CORS 跨域 | Filter 或 MVC 配置 | 预检请求不进入 Controller |
| 登录、权限校验 | Interceptor | 能拿到 HandlerMethod 上的注解 |
| 接口耗时统计 | Interceptor | 能精确到方法级别 |
| 租户上下文切换 | Interceptor | 需要按注解或路径区分 |
| 响应压缩、XSS 过滤 | Filter | 面向原始请求/响应 |
有一个判断技巧:如果你需要"方法上的注解信息",选 Interceptor;如果你需要"在 Spring MVC 之外也生效",选 Filter。
还有一点容易被忽略:Filter 工作在 Spring MVC 之外,所以 Spring Security 这类框架本身就是以 Filter 链的形式接入的。项目里已经用了 Security,登录校验优先交给它,自己再叠加一个 Interceptor 容易和框架的上下文顺序打架。反过来,如果只是几个接口的按钮级权限判断,用 Interceptor 读方法上的注解更顺手。
各写一个
先看 Filter。用 OncePerRequestFilter 避免 forward/include 时重复执行:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (!StringUtils.hasText(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
response.setHeader("X-Trace-Id", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}
再看 Interceptor。它由 Spring 管理,可以正常构造注入依赖:
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
public AuthInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
if (!tokenService.valid(request.getHeader("Authorization"))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
return false;
}
return true;
}
}
注册时只拦截业务接口,把登录和公开接口排除掉:
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
public WebConfig(AuthInterceptor authInterceptor) {
this.authInterceptor = authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/api/public/**");
}
}
常见坑
1. 拦截器用 new 创建
registry.addInterceptor(new AuthInterceptor()); // 里面的 tokenService 是 null
手动 new 的对象不受 Spring 管理,@Autowired 不会生效。正确做法是把拦截器声明成 @Component 或 @Bean,再注入进配置类。
2. 跨域预检被拦截
浏览器发 OPTIONS 预检请求时,路径同样会经过拦截器。如果拦截器直接返回 401,浏览器控制台会报跨域失败。要么放行 OPTIONS,要么用 CorsFilter 在更外层处理。
3. 静态资源和错误页也被拦
/favicon.ico、/error、Swagger 静态资源都可能命中拦截器。除了 excludePathPatterns,还可以像上面的例子一样判断 handler instanceof HandlerMethod,非 Controller 方法直接放行。
4. Filter 读请求体后 Controller 读不到
request.getInputStream() 只能读一次。Filter 里为了打日志把 body 读掉,Controller 再读就是空字符串。要保留请求体,需要用 ContentCachingRequestWrapper 包装,并让下游从缓存的字节数组里取数据;如果只是记日志,记录长度、摘要或者不读 body 更省事。
5. 异步请求的回调不执行
Controller 返回 Callable、DeferredResult 或 WebFlux 时,postHandle 和 afterCompletion 的时机和同步请求不同。需要实现 AsyncHandlerInterceptor 的 afterConcurrentHandlingStarted,否则清理逻辑会漏掉。
6. 异常处理范围不同
Filter 里抛出的异常不经过 Spring MVC 的异常处理链,@RestControllerAdvice 通常接不住;Interceptor 在 preHandle 抛出的异常可以进入 HandlerExceptionResolver,但 afterCompletion 里的异常不会。所以鉴权失败时建议直接写响应并返回 false,而不是抛异常。
7. 顺序混乱
多个 Filter 的顺序用 @Order 或 FilterRegistrationBean 控制;多个 Interceptor 的顺序在注册时用 .order(n) 控制。preHandle 按正序执行,postHandle 和 afterCompletion 按逆序执行,日志里看到的顺序和直觉可能相反。
排查步骤
出现"该拦的没拦、不该拦的拦了",先打一行日志确认执行顺序:
log.info("filter={} uri={}", this.getClass().getSimpleName(), request.getRequestURI());
然后按路径确认:请求有没有命中 addPathPatterns,有没有被 excludePathPatterns 排除,handler 是不是 HandlerMethod。如果涉及跨域,用 curl -X OPTIONS -i 单独测预检;如果涉及响应,看是不是在 chain.doFilter 之前就把 response 提交了。定位清楚层级,再决定把逻辑放在 Filter 还是 Interceptor。
评论