本节目标:把请求在进入 handler 前后穿过的两层关卡(Filter 与 Interceptor)讲清,并把异常解析的完整链路与「哪一层能兜住异常」的边界划出来。
适用版本:Spring Boot 4.1.x(Java 21)
4.3 过滤器、拦截器与异常解析
前两节讲了请求「进到 handler 之后」发生什么,这一节往前退一步:请求到达 DispatcherServlet 之前要过 Filter,到达 handler 之前要过 Interceptor。两层关卡职责相似但能力不同,异常处理又横跨两层——这是最容易出「为什么我的异常处理没生效」的地方。仍用图书借阅服务:给 LibraryController 加一个审计拦截器,再加一个全局异常处理器。
4.3.1 Filter 与 HandlerInterceptor 的分层
Filter 属于 Servlet 规范,不属于 Spring MVC。 它由 jakarta.servlet.Filter 定义,运行在Servlet 容器(Tomcat 11.0.24)里,在请求进入 DispatcherServlet 之前、响应离开之后执行:
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
default void destroy() {}
}
Spring 提供了 OncePerRequestFilter 抽象类简化实现(位于 org.springframework.web.filter,已从 spring-web-7.0.9.jar 核实):
public abstract class OncePerRequestFilter extends GenericFilterBean {
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { ... }
protected abstract void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException;
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { ... }
protected boolean shouldNotFilterAsyncDispatch() { ... }
protected boolean shouldNotFilterErrorDispatch() { ... }
}
HandlerInterceptor 属于 Spring MVC,运行在 DispatcherServlet.doDispatch() 内部(上一节 4.1.4),能拿到 Object handler(实际是 HandlerMethod)与 ModelAndView。
两者的能力差异可以对照成一张表:
| 维度 | Filter | HandlerInterceptor |
|---|---|---|
| 所属规范 | Servlet(jakarta.servlet) | Spring MVC(org.springframework.web.servlet) |
| 运行位置 | 容器层,DispatcherServlet 之外 | MVC 层,DispatcherServlet.doDispatch() 内 |
| 能拿到的 handler | 不能(只有 ServletRequest) | 能(HandlerMethod,可读方法注解) |
能拿到的 ModelAndView | 不能 | 能(postHandle) |
| 覆盖范围 | 所有进容器的请求(含静态资源、error dispatch) | 仅被 handler 匹配到的请求 |
| 路径匹配 | 靠 FilterRegistrationBean 的 URL pattern | 靠 addPathPatterns(MappedInterceptor) |
| 异常可见性 | 在 MVC 之外,拿不到 @ExceptionHandler 处理结果 | preHandle 抛异常可被 @ExceptionHandler 处理 |
| 注册方式 | FilterRegistrationBean / @Bean Filter | WebMvcConfigurer.addInterceptors |
一句话选择标准:需要看 HTTP 原始字节、要给所有请求(含静态资源)加逻辑、要改请求包装(HttpServletRequestWrapper)→ Filter;需要读目标方法注解、需要 ModelAndView、只想作用于控制器 → Interceptor。
4.3.2 两层的注册与顺序控制
注册拦截器用 WebMvcConfigurer.addInterceptors(InterceptorRegistry),返回的 InterceptorRegistration 支持链式配置:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuditInterceptor())
.addPathPatterns("/books/**")
.excludePathPatterns("/books/health")
.order(10);
}
}
InterceptorRegistry 内部把每个拦截器包成 InterceptorRegistration,最终产出 MappedInterceptor——addPathPatterns / excludePathPatterns 都编译进它的路径匹配器,所以在 4.1.4 说的「组装阶段过滤」指的就是这里。多个拦截器的执行顺序由 order 决定(数字小的先 preHandle、后 postHandle/afterCompletion);不显式设置时按注册顺序,order 默认 0,同序时后注册的排后。
注册 Filter 有两种方式。方式一是 @Bean 直接返回 Filter(Spring Boot 自动包装成 FilterRegistrationBean,@Order 或 Ordered 决定顺序);方式二是显式声明 FilterRegistrationBean,能精确控制 URL pattern 与 dispatcher 类型:
@Bean
public FilterRegistrationBean<AuditFilter> auditFilter() {
FilterRegistrationBean<AuditFilter> reg = new FilterRegistrationBean<>(new AuditFilter());
reg.addUrlPatterns("/books/*");
reg.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
return reg;
}
注意 Spring Boot 自动配置的若干 Filter(如 OrderedCharacterEncodingFilter、OrderedFormContentFilter,位于 spring-boot-servlet 模块的 org.springframework.boot.servlet.filter 包)都带固定 order 值;自定义 Filter 的 order 要据此权衡,否则可能排在编码过滤器之前导致乱码。
4.3.3 一次请求穿过两层的顺序
对 /books/{isbn} 这样一个被 handler 匹配到的请求,完整顺序是:
Filter.doFilter 前置
└─ DispatcherServlet.doService → doDispatch
├─ HandlerExecutionChain.applyPreHandle (正序)
├─ HandlerAdapter.handle → 控制器方法
├─ HandlerExecutionChain.applyPostHandle (逆序)
└─ processDispatchResult → render / 异常解析
└─ Filter.doFilter 后置
注意三个「不对称」:
preHandle正序、postHandle与afterCompletion逆序——像栈一样配对。preHandle返回false会短路postHandle,但仍会触发已执行过的拦截器的afterCompletion。- Filter 的「后置代码」在
DispatcherServlet返回后才跑;即使 MVC 层抛异常,只要异常被@ExceptionHandler处理成正常响应,Filter 是感知不到这次异常的。
4.3.4 异常解析链:HandlerExceptionResolver 的顺序
当 doDispatch() 捕获到异常,会进入 processDispatchResult() → processHandlerException()。后者遍历 handlerExceptionResolvers,取第一个返回非 null 的(7.0.9 源码):
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) { /* 处理渲染 */ }
throw ex; // 全部返回 null 就继续抛出
默认三个解析器的顺序由 WebMvcConfigurationSupport.addDefaultHandlerExceptionResolvers() 决定(7.0.9 源码,顺序固定):
| 顺序 | 解析器 | 负责 |
|---|---|---|
| 1 | ExceptionHandlerExceptionResolver | @ExceptionHandler(含 @ControllerAdvice) |
| 2 | ResponseStatusExceptionResolver | @ResponseStatus 注解、ResponseStatusException |
| 3 | DefaultHandlerExceptionResolver | Spring MVC 内建的标准异常 → 对应 HTTP 状态码 |
它们被包进一个 HandlerExceptionResolverComposite(setOrder(0)),作为单个 HandlerExceptionResolver bean 暴露给 DispatcherServlet。顺序即优先级:@ExceptionHandler 永远最先有机会,所以自定义处理器能覆盖内建行为。
4.3.5 @ControllerAdvice + @ExceptionHandler 怎么被找到
ExceptionHandlerExceptionResolver 继承 AbstractHandlerMethodExceptionResolver,它自己实现了 resolveException,核心是 getExceptionHandlerMethod(handlerMethod, exception, webRequest)(7.0.9 源码逻辑):
- 先找控制器本地的处理器:用
handlerMethod.getBeanType()建一个ExceptionHandlerMethodResolver,在该控制器类内部找匹配异常的@ExceptionHandler方法。找到就直接返回。 - 再找全局 advice:遍历
exceptionHandlerAdviceCache(在afterPropertiesSet()里由ControllerAdviceBean.findAnnotatedBeans(context)建立),对每个@ControllerAdvice调用advice.isApplicableToBeanType(handlerType)判断是否适用,再在其内找匹配异常的@ExceptionHandler。
isApplicableToBeanType 的判据来自 @ControllerAdvice 的属性(已核实注解定义):
public @interface ControllerAdvice {
String name() default "";
String[] value() default {}; // 别名 basePackages
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {}; // 只作用于这些类型
Class<? extends Annotation>[] annotations() default {}; // 只作用于带这些注解的控制器
}
所以「全局异常处理器为什么没生效」的常见原因是 assignableTypes / basePackages 把目标控制器排除了。@RestControllerAdvice 只是 @ControllerAdvice + @ResponseBody 的组合。
匹配粒度还有一层:ExceptionHandlerMethodResolver 内部维护「异常类型 → 处理方法」的映射,同一个异常若有多个候选(父类、子类),选最具体的异常类型匹配。方法参数除了异常本身,还可以声明 HttpServletRequest、WebRequest 等——由解析器注入。
4.3.6 @ResponseStatus 与 ResponseStatusException
当 @ExceptionHandler 没匹配上,轮到 ResponseStatusExceptionResolver。它继承 AbstractHandlerExceptionResolver,覆写 doResolveException(),内部走两条路:
resolveResponseStatus(...):处理异常类(或其父类)上的@ResponseStatus注解。注解有三个属性(已核实):value()(别名code(),HttpStatus)与reason()。resolveResponseStatusException(...):处理ResponseStatusException实例。该类位于org.springframework.web.server,7.0.9 里继承自ErrorResponseException:
public class ResponseStatusException extends ErrorResponseException {
public ResponseStatusException(HttpStatusCode status) { ... }
public ResponseStatusException(HttpStatusCode status, String reason) { ... }
public String getReason();
public HttpHeaders getHeaders();
}
ResponseStatusException 的好处是状态码可以运行时决定——throw new ResponseStatusException(HttpStatus.NOT_FOUND, "book not found"),不必为每种错误定义一个异常类。它还能带 HttpHeaders(如 Retry-After)。
两条路最终都调 applyStatusAndReason(status, reason, response) 写状态码。若 reason 非空且响应未提交,还会把 reason 写进响应体。
还有一个容易忽略的用法:@ResponseStatus 可以直接标在 @ExceptionHandler 方法上。这种情况下它不是由 ResponseStatusExceptionResolver 解析,而是在 ServletInvocableHandlerMethod.invokeAndHandle() 里通过 setResponseStatus(webRequest) 应用——也就是在第 1 个解析器(ExceptionHandlerExceptionResolver)内部就写好了状态码,根本轮不到第 2 个。断点位置因此不同:前者下在 ExceptionHandlerExceptionResolver,后者下在 ResponseStatusExceptionResolver。
若前两个解析器都没命中,DefaultHandlerExceptionResolver 兜底:它把 Spring MVC 的内建异常映射成标准状态码。7.0.9 里它先判断 ex instanceof ErrorResponse(ResponseStatusException 之外的框架异常都实现了该接口),逐类转成 HttpRequestMethodNotSupportedException → 405、HttpMediaTypeNotSupportedException → 415、MethodArgumentNotValidException → 400、NoResourceFoundException → 404 等;非 ErrorResponse 的(TypeMismatchException、HttpMessageNotReadableException 等)走第二段判断。
4.x 新变化:Spring Boot 4 在 spring-boot-webmvc 里自动配置了一个 ProblemDetailsExceptionHandler(org.springframework.boot.webmvc.autoconfigure,类上带 @ControllerAdvice,继承 ResponseEntityExceptionHandler)。它把上述内建异常统一渲染成 RFC 9457 的 ProblemDetail。该 bean 由 spring.mvc.problemdetails.enabled 开关控制(源码中 @ConditionalOnBooleanProperty 已核实),默认行为随属性而定——写 4.x 时不要再照抄 3.x 的 ErrorAttributes 单独配置思路。
4.3.7 异常在 Filter 与 Interceptor 两层的边界
这是本节最实用的部分:异常在哪里抛,决定了它能不能被 @ExceptionHandler 接住。
| 抛出位置 | 能否被 @ExceptionHandler 处理 | 原因 |
|---|---|---|
| 控制器方法内 | 能 | 在 doDispatch() 的 try 内,进 processDispatchResult |
| 参数解析 / 消息转换 | 能 | 同上,ha.handle() 抛出的异常被捕获 |
Interceptor.preHandle | 能 | 在 doDispatch() 的 try 内(第 ③ 段) |
Interceptor.postHandle | 能 | 在同一个 try 内(第 ⑥ 段) |
Interceptor.afterCompletion | 不能 | 在 processDispatchResult 之后,已在 try 之外 |
Filter.doFilter 内 | 不能 | Filter 在 DispatcherServlet 之外,@ExceptionHandler 根本看不到 |
关键结论:
- Interceptor 的
preHandle抛异常会被当作 dispatch 异常处理,因为它发生在doDispatch()的主 try 块里。这是「拦截器里做鉴权失败就throw」能拿到统一错误体的原因。 - Filter 抛的异常走不到 MVC 的异常链,只能靠容器错误页(
/error→BasicErrorController)或 Filter 自己 try-catch。而且/error转发是一次DispatcherType.ERROR的新 dispatch,会再次穿过 Filter 链(OncePerRequestFilter默认的shouldNotFilterErrorDispatch()返回true,正是为了避免重复执行)。 afterCompletion抛异常不会改变已写好的响应,只会被DispatcherServlet记录/抛出到容器——所以别在afterCompletion里做「可能失败且影响结果」的操作。
4.3.8 知道之后能做什么
排障:@ExceptionHandler 不生效。 按顺序自查:① 异常是不是在 Filter 里抛的(那就永远进不来);② @ControllerAdvice 的 basePackages / assignableTypes 是否排除了当前控制器;③ 方法返回类型是否被 RequestResponseBodyMethodProcessor 认领(@RestControllerAdvice 或方法上加 @ResponseBody);④ 是否有更靠前的解析器(ExceptionHandlerExceptionResolver 本身)先命中了别的处理器。
排障:状态码不对。 用断点下在 ResponseStatusExceptionResolver.doResolveException(),看 @ResponseStatus 的 reason() 与 value() 是否被读对;ResponseStatusException 场景断点下在构造函数,确认状态码是运行时算出来的那个。
扩展:统一错误体。 写一个 @RestControllerAdvice,用 @ExceptionHandler 覆盖业务异常,返回自定义 DTO;或让业务异常实现 ErrorResponse 接口,直接暴露 ProblemDetail,交给 4.x 的 ProblemDetailsExceptionHandler 渲染。
扩展:Filter 与 Interceptor 的分工。 审计拦截器(读方法上的 @Audit 注解)用 Interceptor;请求体加解密、TraceId 注入、ContentCachingRequestWrapper 这类需要包装原始请求的用 Filter。
验证:观察拦截器顺序。 给审计拦截器与另一个 WebMvcConfigurer 注册的拦截器各打印一行,会看到 preHandle 按注册顺序、postHandle/afterCompletion 逆序。
小结
- Filter 在 Servlet 容器层、
DispatcherServlet之外;Interceptor 在 Spring MVC 层、doDispatch()之内,能拿到HandlerMethod与ModelAndView。 - 拦截器
preHandle正序、postHandle/afterCompletion逆序;preHandle返回false会短路后续段。 - 异常解析链固定为
ExceptionHandlerExceptionResolver→ResponseStatusExceptionResolver→DefaultHandlerExceptionResolver,第一个返回非 null 的胜出。 @ExceptionHandler先找控制器本地方法,再找适用的@ControllerAdvice;@ControllerAdvice的basePackages/assignableTypes/annotations决定适用范围。@ResponseStatus与ResponseStatusException由ResponseStatusExceptionResolver处理;后者继承ErrorResponseException,状态码可运行时决定。- 控制器、参数解析、
preHandle/postHandle抛的异常能被@ExceptionHandler接住;afterCompletion与 Filter 内抛的不能。
到这里,Servlet 栈的请求处理管线就讲完了。下一章转入响应式:同样的请求处理问题,在 WebFlux 里由完全不同的类型体系(Mono/Flux)与背压模型承担。
阅读导航:上一节:4.2 参数解析与消息转换 · 下一节:5.1 响应式类型与背压 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。