很多人初次在Spring Boot项目中编写全局异常处理时,都会产生一个错觉:“我都用上 @RestControllerAdvice 了,理论上所有的异常都应该被我捕获和处理。” 然而现实往往很骨感——有的异常能被成功拦截,有的却死活进不来;有的在本地开发环境能捕获,到了线上却失灵;你甚至可能调试半天,最终发现异常压根没走到你精心编写的处理器代码。
这并非玄学,而是因为Spring Boot的异常处理机制本身是分层的。理解这一点,是解决“异常兜不住”问题的关键。

全局异常处理的生效边界
典型的全局异常处理器代码如下:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
return Result.fail(e.getMessage());
}
}
开发者常误以为:连最顶层的 Exception 都捕获了,还有什么异常进不来?
但很快你就会遇到以下棘手情况:
- 参数校验失败,前端直接收到400响应,你的处理器没反应。
- 接口路径写错,返回404,处理器没反应。
- 请求方式不对(如GET请求了POST接口),返回405,处理器没反应。
- 前端传递了格式错误的JSON,请求连Controller都没进就返回400,处理器依然没反应。
- 在自定义过滤器(Filter)里抛出了异常,全局处理器完全感知不到。
于是,你开始怀疑自己的代码,甚至怀疑人生。

异常触发的层级差异
这是最核心的一点。一个HTTP请求在Spring Boot中的处理流程大致如下:

而你使用 @RestControllerAdvice 和 @ExceptionHandler 定义的全局异常处理器,默认只对进入Controller层之后抛出的异常生效。
这意味着什么?
- Controller之前抛出的异常,你的处理器是接不到的。 这包括了Filter、Interceptor、参数解析(ArgumentResolver)等阶段的异常。
- Spring框架自身处理的某些内置异常,也不一定会抛给你的处理器。
所以,问题的根源往往不是你写错了处理器,而是异常根本就没到达你处理器所能管辖的那一层。

参数校验异常的处理方式
例如,你在Controller中使用了参数校验:
@PostMapping("/save")
public void save(@Valid @RequestBody UserDTO dto) {
}
当校验失败时,客户端会收到400状态码,但你的全局 Exception.class 处理器却毫无动静。
原因在于:参数校验失败抛出的异常并非普通的 Exception,而是特定的子类,例如:
MethodArgumentNotValidException (用于@RequestBody校验)
BindException (用于@ModelAttribute校验)
ConstraintViolationException (用于方法参数校验,如@Validated)
并且,这些异常在参数解析(ArgumentResolver)阶段就已经被抛出,早于Controller方法的执行。
解决方案很明确:在你的全局异常处理器中,单独捕获这些特定异常。
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult()
.getFieldError()
.getDefaultMessage();
return Result.fail(msg);
}
这是第一类“看起来进不来,其实是你没接对异常类型”的情况。

404 / 405 异常的拦截机制
这是第二个高频误区。
- 404 (Not Found):请求的路径不存在。
- 405 (Method Not Supported):请求的HTTP方法不匹配(如用GET访问只支持POST的接口)。
这两个异常默认不会以抛出异常的方式传递到Controller层,而是被Spring MVC在更早的请求匹配阶段直接处理,并返回对应的错误页面或状态码。
所以,无论你写多少个 @ExceptionHandler 都无济于事,因为异常根本没被“抛”出来。
要让这些异常能够被你的全局处理器捕获,必须进行显式配置:
spring:
mvc:
# 关键配置:当没有找到请求处理器时,抛出异常而非返回404页面
throw-exception-if-no-handler-found: true
web:
resources:
# 关闭静态资源映射,否则对静态资源的404请求不会被抛出
add-mappings: false
配置之后,你才能在全局异常处理器中捕获对应的异常:
@ExceptionHandler(NoHandlerFoundException.class)
public Result<?> handle404(NoHandlerFoundException e) {
return Result.fail("接口不存在");
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<?> handle405(HttpRequestMethodNotSupportedException e) {
return Result.fail("请求方法不支持");
}
这一点很多人不知道,问题常常是不是不会写处理器,而是Spring默认没给你处理的机会。

请求体解析异常的触发时机
当客户端传了一个无法解析的JSON时,例如将字符串赋给数字字段:
{ "age": "abc" }
后端会直接返回400,如果你在Controller方法入口打上断点,会发现一次都没进去。
这是因为异常发生在 HttpMessageConverter (消息转换器)解析请求体的阶段,此时Controller尚未执行。
对应的异常是:
HttpMessageNotReadableException
解决方案依然是:在全局处理器中明确捕获它。
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<?> handleJsonError(HttpMessageNotReadableException e) {
return Result.fail("请求参数格式错误");
}
至此,我们可以总结出一个规律:“接不到异常” ≠ “异常不存在”,往往是“异常发生的层级不对”或“异常的类型你没捕获”。

Filter / Interceptor 异常的处理策略
这是最容易踩的深坑。如果你在以下组件中直接抛出异常:
Filter
OncePerRequestFilter
HandlerInterceptor
并且简单地 throw new RuntimeException(...),那么你的 @RestControllerAdvice 是捕获不到这个异常的。
原因在于,此时请求还未正式进入Spring MVC的DispatcherServlet处理流程,负责调用 @ExceptionHandler 的解析器尚未介入。
对于这类“前置”异常,正确的处理方式通常有两种:
方案一:在过滤器(Filter)或拦截器(Interceptor)内部自行处理响应
try {
chain.doFilter(request, response);
} catch (Exception e) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write("{\"code\":500,\"msg\":\"过滤器内部错误\"}");
}
方案二:将异常转交给Spring的异常处理器(更推荐)
此方法可以复用你已定义的全局异常处理逻辑。
@Autowired
private HandlerExceptionResolver handlerExceptionResolver;
// 在catch块中
catch (Exception e) {
handlerExceptionResolver.resolveException(
request, response, null, e
);
}
第二种是更工程化、更解耦的解法,它让Filter/Interceptor中的异常也能被统一的 @RestControllerAdvice 处理,许多成熟的Java项目都采用这种方式。


小结
全局异常处理的本质不是简单的“兜底”,而是 “分层兜底” 。一旦你建立起“异常发生在Spring请求处理流程的哪一层”的清晰认知,就不会再困惑于“为什么我的处理器接不到这个异常”。
你需要做的是:
- 识别异常源头:判断异常是在Filter、参数解析、Controller还是Service层抛出。
- 精确捕获类型:不要只依赖
Exception.class,针对常见的特定异常(如校验、404、消息解析)编写专门的处理器。
- 配置框架行为:对于像404这样的异常,需要修改Spring Boot的默认配置才能使其抛出。
- 统一前置异常:对于后端 & 架构层面的组件(如Filter)抛出的异常,考虑使用
HandlerExceptionResolver 将其路由到统一的处理流程。
理解并处理好这些边界情况,你的全局异常处理器才能真正做到“全局”可控。希望这篇文章能帮助你理清思路。在实际项目中遇到棘手的异常捕获问题时,不妨从请求生命周期的角度重新审视。欢迎在云栈社区交流讨论更多解决方案。