找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2040

积分

0

好友

269

主题
发表于 昨天 05:44 | 查看: 7| 回复: 0

很多人初次在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中的处理流程大致如下:

Spring MVC请求处理流程图

而你使用 @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请求处理流程的哪一层”的清晰认知,就不会再困惑于“为什么我的处理器接不到这个异常”。

你需要做的是:

  1. 识别异常源头:判断异常是在Filter、参数解析、Controller还是Service层抛出。
  2. 精确捕获类型:不要只依赖 Exception.class,针对常见的特定异常(如校验、404、消息解析)编写专门的处理器。
  3. 配置框架行为:对于像404这样的异常,需要修改Spring Boot的默认配置才能使其抛出。
  4. 统一前置异常:对于后端 & 架构层面的组件(如Filter)抛出的异常,考虑使用 HandlerExceptionResolver 将其路由到统一的处理流程。

理解并处理好这些边界情况,你的全局异常处理器才能真正做到“全局”可控。希望这篇文章能帮助你理清思路。在实际项目中遇到棘手的异常捕获问题时,不妨从请求生命周期的角度重新审视。欢迎在云栈社区交流讨论更多解决方案。




上一篇:InputPlumber安全漏洞曝光:Linux游戏玩家面临会话劫持风险
下一篇:企业SRC实战:培训平台弱口令攻击导致文件未授权访问漏洞
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 10:56 , Processed in 0.306993 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表