面试知识库
基础

SpringMVC统一异常处理#

一句话答案#

@RestControllerAdvice + @ExceptionHandler 做全局异常处理,把 Controller 抛出的异常集中转换成统一响应体;底层由 DispatcherServlet 的 HandlerExceptionResolver(DefaultHandler、ExceptionHandlerExceptionResolver 等)在请求处理出错后回调。

核心要点

首选方案:@RestControllerAdvice(= @ControllerAdvice + @ResponseBody)

匹配规则: 优先匹配最具体的异常类型;同一异常多个 handler 时选最近的父子距离;@ControllerAdvice 可用 basePackages/assignableTypes 限定作用范围。

底层原理: Controller 抛异常后,DispatcherServlet 不直接抛给容器,而是遍历 HandlerExceptionResolver 链处理:

  • ExceptionHandlerExceptionResolver → 处理 @ExceptionHandler(这是 @ControllerAdvice 生效的关键)
  • ResponseStatusExceptionResolver → 处理 @ResponseStatus / ResponseStatusException
  • DefaultHandlerExceptionResolver → 处理 SpringMVC 标准异常(如 405、415)

注意边界: @ControllerAdvice 只能拦截进入 DispatcherServlet 之后、Controller 链路里的异常。Filter 层抛的异常(如鉴权 Filter)到不了 ControllerAdvice,需要在 Filter 里自行处理或交给 Spring Security 的 ExceptionTranslationFilter。

面试回答(2分钟版)

SpringMVC 做统一异常处理最常用的是 @RestControllerAdvice 配合 @ExceptionHandler。@RestControllerAdvice 其实就是 @ControllerAdvice 加上 @ResponseBody,作用是定义一个全局的、横切所有 Controller 的增强类。我会在里面针对不同异常类型写多个 @ExceptionHandler 方法,比如自定义的业务异常返回业务错误码,参数校验失败的 MethodArgumentNotValidException 返回 400 和具体校验信息,最后再写一个处理 Exception 的兜底方法,记录日志并返回一个友好的提示,避免把堆栈直接暴露给前端。匹配时 Spring 会选最具体的异常类型。它的底层原理是 DispatcherServlet 在 Controller 抛异常后不会直接把异常抛给 Servlet 容器,而是遍历一组 HandlerExceptionResolver 来处理,其中 ExceptionHandlerExceptionResolver 专门负责找到匹配的 @ExceptionHandler 方法来执行,这就是 ControllerAdvice 能生效的原因。需要特别注意的是,这套机制只能覆盖进入 DispatcherServlet 之后的异常,如果异常是在 Filter 层抛出来的,比如鉴权过滤器,那它根本到不了 ControllerAdvice,需要在 Filter 里单独处理,或者交给 Spring Security 自己的异常处理过滤器。

追问与易错

追问方向:

  • “@ControllerAdvice 和 @RestControllerAdvice 区别?”→ 后者等于前者加 @ResponseBody,方法返回值直接序列化为 JSON;做 REST 接口用 @RestControllerAdvice,返回视图时用 @ControllerAdvice
  • “全局异常处理能捕获 Filter 里抛的异常吗?”→ 不能,@ControllerAdvice 只在 DispatcherServlet 内部的 Controller 链路生效;Filter 早于 DispatcherServlet,异常要在 Filter 内处理或由 Security 的 ExceptionTranslationFilter 接管
  • “@ExceptionHandler 匹配多个异常类型怎么选?”→ 选与抛出异常继承距离最近、最具体的那个;可在一个注解里写多个异常类,也可用一个父类异常兜底
  • “怎么统一处理参数校验异常?”→ 捕获 MethodArgumentNotValidException(@RequestBody + @Valid)和 BindException(表单绑定)、ConstraintViolationException(@Validated 方法参数),从 BindingResult 取出字段错误信息
  • “异常处理里怎么返回正确的 HTTP 状态码?”→ 方法上加 @ResponseStatus,或返回 ResponseEntity 自定义状态码,否则默认 200 容易误导前端
  • “还有别的全局异常处理方式吗?”→ 实现 HandlerExceptionResolver 接口、用 ErrorController(Boot 的 /error 兜底),但 @RestControllerAdvice 最常用、可读性最好

易错点:

  • ❌ 以为 @ControllerAdvice 能兜住所有异常——拦不到 Filter、异步线程、以及 Controller 之外的异常
  • ❌ 兜底方法直接返回 e.getMessage() 或堆栈——可能泄漏敏感信息,应记录日志后返回脱敏提示
  • ❌ 忘记设置 HTTP 状态码,全部返回 200,前端无法用状态码判断成败