Spring中过滤器和拦截器的区别是什么?

两个东西都是"横切"用的,写法也像:实现一个接口,把逻辑塞进去,就能在 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 回调。

相关推荐
M1A11 小时前
VS Code 拉取 Gitee 代码全攻略:从零到一,新手也能轻松搞定!
后端
Mikko71 小时前
JVM 线上排查实战(六):JFR 怎么用?JDK 8 要不要加 UnlockCommercialFeatures、jfr 命令在哪、录的文件为什么读不了
java·运维·jvm·后端
小米里的大麦2 小时前
C++ 移动语义
c++·后端
kele_z2 小时前
按部门控制数据权限,一个注解 + 一个切面 + 一行 SQL 就够了
后端
一个有温度的技术博主2 小时前
凌晨两点的死锁:当线程“互相等死“
开发语言·后端·场景
ThinkerQAQ_2 小时前
并发编程(八):读写锁——从语言规则到 CPU
后端
lizhongxuan2 小时前
让 AI 提效转化为团队交付
后端
桃西西呀2 小时前
Spring AI Alibaba 之二:Spring AI 底座与原子抽象一笔带过,看清 SAA 在之上加了什么编排
人工智能·spring·llm
颜进强2 小时前
23 · NestJs ModuleRef 模块引用:容器递给你的"取货窗口",四个 API 四种语义
前端·后端·ai编程