两个东西都是"横切"用的,写法也像:实现一个接口,把逻辑塞进去,就能在 Controller 前后插一脚。
区别在站在哪一层。Filter 来自 Servlet 规范,是 Servlet 容器(Tomcat)调用的;Interceptor 是 Spring MVC 的东西,由 DispatcherServlet 调用。所以 Filter 在 DispatcherServlet 外面,Interceptor 在 DispatcherServlet 里面,后面那些差别基本都能落到这个位置上。
请求进来的那条链
我们先把请求进来之后的链路画出来:

Filter
java
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
default void destroy() {}
}
三个方法里只有 doFilter 是必须实现的。init 和 destroy 分别在这个 Filter 第一次用到和容器关闭时各调一次。
doFilter 的放行方式就是调 chain.doFilter(request, response),不调就相当于拦住了,请求不会往下走。下面这几行代码是 Filter 最常见的一个形态:
java
@Component
public class CostFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
System.out.println("cost " + (System.currentTimeMillis() - start) + "ms");
}
}
}
耗时统计写在 chain.doFilter 前后,因为这一句里面才是后面整条链(包括 DispatcherServlet)。
拿到的 ServletRequest 是容器给的原始对象,getRequestURI()、getHeader() 这些都得自己 cast 成 HttpServletRequest。想往里加东西(比如记一个 traceId 往下传),只能自己包一层 HttpServletRequestWrapper:
java
HttpServletRequest wrapper = new HttpServletRequestWrapper((HttpServletRequest) request) {
@Override
public String getHeader(String name) {
if ("X-Trace-Id".equals(name)) {
return traceId;
}
return super.getHeader(name);
}
};
chain.doFilter(wrapper, response);
响应同理,HttpServletResponseWrapper 包一层再传下去。请求体也是这个套路,ContentCachingRequestWrapper / ContentCachingResponseWrapper 就是 Spring 给的现成 wrapper。
这里提醒一句:请求体只能在 Filter 里读一次。如果你的 Filter 里调了
request.getInputStream()把 body 读出来打印,后面@RequestBody拿到的就是空字符串。要么用ContentCachingRequestWrapper包一层,要么在 Filter 里别碰 body。
jakarta.servlet 还是 javax.servlet 看 Spring Boot 版本。Boot 3 跟着 Tomcat 10 换成了 jakarta.*,从旧项目拷 Filter 的时候这里必改。
注册
三种方式:
java
// 1. 直接标 @Component,Spring Boot 会把它注册到 /*
@Component
public class CostFilter implements Filter { ... }
// 2. FilterRegistrationBean,能指定 urlPatterns、顺序、dispatcher type
@Bean
public FilterRegistrationBean<CostFilter> costFilter() {
FilterRegistrationBean<CostFilter> bean = new FilterRegistrationBean<>(new CostFilter());
bean.addUrlPatterns("/api/*");
bean.setOrder(1);
return bean;
}
// 3. 走 Servlet 原生的 @WebFilter,需要在启动类上加 @ServletComponentScan
@WebFilter(urlPatterns = "/*")
public class CostFilter implements Filter {
@Autowired
private UserService userService; // 注入不进去,见下面
}
第三种要注意,@WebFilter 标注的 Filter 由 Servlet 容器 new 出来,不是 Spring 容器里的 bean,@Autowired 不生效。
Interceptor
java
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 方法执行前 | handler 是 HandlerMethod,返回 false 就不往下走了 |
postHandle |
Controller 执行完、视图渲染前 | 多了 ModelAndView,可以往里塞东西 |
afterCompletion |
整个请求结束(视图渲染完) | ex 是这次请求抛出的异常,没异常就是 null |
@RequestMapping 匹配到的方法,在这里就是 HandlerMethod,能拿到 Controller 类和 Method 对象,也能读它上面的注解。所以"只拦带某个注解的接口"这种需求只有拦截器能做:
java
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true; // 静态资源这类不是 HandlerMethod,直接放行
}
if (!handlerMethod.hasMethodAnnotation(RequireLogin.class)) {
return true;
}
String token = request.getHeader("Authorization");
if (token == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}
}
注册
拦截器不能标个注解就生效,得往 InterceptorRegistry 里注册:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/error", "/static/**")
.order(1);
}
}
preHandle 返回 false 之后:请求直接返回,postHandle 不执行;但已经通过 preHandle 的拦截器,它们的 afterCompletion 还是会执行,ex 传 null。这一点和 Filter 里"不放行就整个断掉"不一样,资源释放写在 afterCompletion 里是安全的。
对比
Filter |
Interceptor |
|
|---|---|---|
| 所属 | Servlet 规范(jakarta.servlet) |
Spring MVC |
| 谁调用 | Servlet 容器 | DispatcherServlet |
| 实现接口 | Filter |
HandlerInterceptor |
| 入口 | doFilter(req, resp, chain) 一个方法 |
preHandle / postHandle / afterCompletion |
| 放行方式 | chain.doFilter(),不调就断 |
preHandle 返回 true |
| 拦的范围 | 容器收到的所有请求 | DispatcherServlet 找得到 handler 的请求 |
| 看得到 Controller 方法 | 看不到 | 能,handler 就是 HandlerMethod |
| 包装 request / response | 能,包一层再往下传 | 不能,只能直接往 response 里写 |
| 抛异常的归宿 | 容器错误页(Boot 的 /error) |
HandlerExceptionResolver,@ControllerAdvice 能接 |
| 注册 | FilterRegistrationBean / @Component / @WebFilter |
WebMvcConfigurer#addInterceptors |
| 顺序 | @Order、setOrder(),数字小的先 |
注册顺序,或 .order() |
| 方向 | 按顺序进、按逆序出 | preHandle 正序,postHandle / afterCompletion 逆序 |
几个容易混的点
静态资源归谁拦
@Component 的 Filter 默认映射到 /*,容器收到的请求都会经过它,包括那些压根不由 Spring MVC 处理的:Druid 的 StatViewServlet、H2 的控制台,这些都是独立注册到容器上的 servlet,只有 Filter 拦得到。
拦截器这边,addPathPatterns("/**") 也会拦到静态资源,因为静态资源在 Spring MVC 里也是一个 handler(ResourceHttpRequestHandler)。所以实际项目里写了 /** 之后一般都会补一句 excludePathPatterns("/static/**", "/favicon.ico"),否则登录校验会拦到 CSS 上,页面直接白板。
异常去哪了
DispatcherServlet.doDispatch 里的 try 是从 applyPreHandle 就开始包的,preHandle 抛出来的异常会和 Controller 里抛的一样,走到 processDispatchResult → HandlerExceptionResolver → @ControllerAdvice。所以拦截器里直接 throw new BizException(...) 是能拿到统一响应体的。
Filter 就不行了。它跑在 DispatcherServlet 外面,HandlerExceptionResolver 根本没机会看到,异常一路冒到容器,最后交给 Boot 的错误处理页面(/error),返回的是 BasicErrorController 那个格式。想在 Filter 层也保持统一响应体,就得自己在 Filter 里 catch 住然后手写 JSON,或者干脆别在 Filter 里做校验。
Filter 里能不能 @Autowired
能,但要看这个 Filter 是怎么来的。
@Component 注册的 Filter 本身就是一个 Spring bean,依赖注入、@Value、@ConfigurationProperties 都正常。@WebFilter + @ServletComponentScan 那条路不行,Filter 是容器 new 的,和 Spring 容器没关系。
这就是 DelegatingFilterProxy 存在的原因:它自己是个 Filter(容器 new 出来的没关系,它不需要依赖),doFilter 的时候按名字去 Spring 容器里找真正的 Filter 然后委托过去。
xml
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
Spring Security 的一整条过滤器链就挂在这个名字底下。启动日志里那句 Will secure any request with [...],列出来的那些 SecurityFilterChain 成员,都是被这一层代理转进去的。
顺序
Filter 的顺序由 @Order 或者 FilterRegistrationBean.setOrder() 决定,数字小的先执行、后返回。拦截器的顺序由 addInterceptors 里注册的先后决定,.order() 也可以指定。
preHandle 正序执行、postHandle 和 afterCompletion 逆序执行,这一点和 Filter 的"进去一圈、出来一圈"是一致的,可以照着最上面那张链路图对一遍。
另外 Filter 一定在拦截器之前,因为拦截器的调用方 DispatcherServlet 本身就是 Filter 链末端的一个 servlet。
异步请求
Controller 返回 Callable、DeferredResult 这类异步结果时,DispatcherServlet 会先把请求线程放掉,postHandle 和 afterCompletion 都不会在那一刻调用。afterCompletion 会在异步结果处理完之后回调,postHandle 则整个跳过。
如果我们依赖 postHandle 做清理,异步接口上就会漏掉。要在请求线程被放掉的那一刻做点事,用 AsyncHandlerInterceptor,它比 HandlerInterceptor 多一个 afterConcurrentHandlingStarted 回调。