Spring中过滤器和拦截器的区别是什么?
两个东西都是横切用的写法也像实现一个接口把逻辑塞进去就能在 Controller 前后插一脚。区别在站在哪一层。Filter来自 Servlet 规范是 Servlet 容器Tomcat调用的Interceptor是 Spring MVC 的东西由DispatcherServlet调用。所以 Filter 在DispatcherServlet外面Interceptor 在DispatcherServlet里面后面那些差别基本都能落到这个位置上。请求进来的那条链我们先把请求进来之后的链路画出来FilterpublicinterfaceFilter{defaultvoidinit(FilterConfigfilterConfig)throwsServletException{}voiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException;defaultvoiddestroy(){}}三个方法里只有doFilter是必须实现的。init和destroy分别在这个 Filter 第一次用到和容器关闭时各调一次。doFilter的放行方式就是调chain.doFilter(request, response)不调就相当于拦住了请求不会往下走。下面这几行代码是 Filter 最常见的一个形态ComponentpublicclassCostFilterimplementsFilter{OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{longstartSystem.currentTimeMillis();try{chain.doFilter(request,response);}finally{System.out.println(cost (System.currentTimeMillis()-start)ms);}}}耗时统计写在chain.doFilter前后因为这一句里面才是后面整条链包括DispatcherServlet。拿到的ServletRequest是容器给的原始对象getRequestURI()、getHeader()这些都得自己 cast 成HttpServletRequest。想往里加东西比如记一个 traceId 往下传只能自己包一层HttpServletRequestWrapperHttpServletRequestwrappernewHttpServletRequestWrapper((HttpServletRequest)request){OverridepublicStringgetHeader(Stringname){if(X-Trace-Id.equals(name)){returntraceId;}returnsuper.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 的时候这里必改。注册三种方式// 1. 直接标 ComponentSpring Boot 会把它注册到 /*ComponentpublicclassCostFilterimplementsFilter{...}// 2. FilterRegistrationBean能指定 urlPatterns、顺序、dispatcher typeBeanpublicFilterRegistrationBeanCostFiltercostFilter(){FilterRegistrationBeanCostFilterbeannewFilterRegistrationBean(newCostFilter());bean.addUrlPatterns(/api/*);bean.setOrder(1);returnbean;}// 3. 走 Servlet 原生的 WebFilter需要在启动类上加 ServletComponentScanWebFilter(urlPatterns/*)publicclassCostFilterimplementsFilter{AutowiredprivateUserServiceuserService;// 注入不进去见下面}第三种要注意WebFilter标注的 Filter 由 Servlet 容器new出来不是 Spring 容器里的 beanAutowired不生效。InterceptorpublicinterfaceHandlerInterceptor{defaultbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{returntrue;}defaultvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{}defaultvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{}}三个方法的时机方法时机参数里有什么preHandleController 方法执行前handler是HandlerMethod返回false就不往下走了postHandleController 执行完、视图渲染前多了ModelAndView可以往里塞东西afterCompletion整个请求结束视图渲染完ex是这次请求抛出的异常没异常就是nullRequestMapping匹配到的方法在这里就是HandlerMethod能拿到 Controller 类和Method对象也能读它上面的注解。所以只拦带某个注解的接口这种需求只有拦截器能做publicclassAuthInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{if(!(handlerinstanceofHandlerMethodhandlerMethod)){returntrue;// 静态资源这类不是 HandlerMethod直接放行}if(!handlerMethod.hasMethodAnnotation(RequireLogin.class)){returntrue;}Stringtokenrequest.getHeader(Authorization);if(tokennull){response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);returnfalse;}returntrue;}}注册拦截器不能标个注解就生效得往InterceptorRegistry里注册ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(newAuthInterceptor()).addPathPatterns(/**).excludePathPatterns(/login,/error,/static/**).order(1);}}preHandle返回false之后请求直接返回postHandle不执行但已经通过preHandle的拦截器它们的afterCompletion还是会执行ex传null。这一点和 Filter 里不放行就整个断掉不一样资源释放写在afterCompletion里是安全的。对比FilterInterceptor所属Servlet 规范jakarta.servletSpring MVC谁调用Servlet 容器DispatcherServlet实现接口FilterHandlerInterceptor入口doFilter(req, resp, chain)一个方法preHandle/postHandle/afterCompletion放行方式chain.doFilter()不调就断preHandle返回true拦的范围容器收到的所有请求DispatcherServlet找得到 handler 的请求看得到 Controller 方法看不到能handler就是HandlerMethod包装 request / response能包一层再往下传不能只能直接往response里写抛异常的归宿容器错误页Boot 的/errorHandlerExceptionResolverControllerAdvice能接注册FilterRegistrationBean/Component/WebFilterWebMvcConfigurer#addInterceptors顺序Order、setOrder()数字小的先注册顺序或.order()方向按顺序进、按逆序出preHandle正序postHandle/afterCompletion逆序几个容易混的点静态资源归谁拦Component的 Filter 默认映射到/*容器收到的请求都会经过它包括那些压根不由 Spring MVC 处理的Druid 的StatViewServlet、H2 的控制台这些都是独立注册到容器上的 servlet只有 Filter 拦得到。拦截器这边addPathPatterns(/**)也会拦到静态资源因为静态资源在 Spring MVC 里也是一个 handlerResourceHttpRequestHandler。所以实际项目里写了/**之后一般都会补一句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都正常。WebFilterServletComponentScan那条路不行Filter 是容器new的和 Spring 容器没关系。这就是DelegatingFilterProxy存在的原因它自己是个 Filter容器 new 出来的没关系它不需要依赖doFilter的时候按名字去 Spring 容器里找真正的 Filter 然后委托过去。filterfilter-namespringSecurityFilterChain/filter-namefilter-classorg.springframework.web.filter.DelegatingFilterProxy/filter-class/filterSpring 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回调。