SpringMVC统一异常处理#
一句话答案#
用
@RestControllerAdvice+@ExceptionHandler做全局异常处理,把 Controller 抛出的异常集中转换成统一响应体;底层由 DispatcherServlet 的 HandlerExceptionResolver(DefaultHandler、ExceptionHandlerExceptionResolver 等)在请求处理出错后回调。
核心要点
首选方案:@RestControllerAdvice(= @ControllerAdvice + @ResponseBody)
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(BizException.class)
public Result<Void> handleBiz(BizException e) {
return Result.fail(e.getCode(), e.getMessage());
}
// 处理参数校验异常(@Valid 失败)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(400, msg);
}
// 兜底,避免堆栈泄漏给前端
@ExceptionHandler(Exception.class)
public Result<Void> handleAll(Exception e) {
log.error("未处理异常", e);
return Result.fail(500, "系统繁忙");
}
}java匹配规则: 优先匹配最具体的异常类型;同一异常多个 handler 时选最近的父子距离;@ControllerAdvice 可用 basePackages/assignableTypes 限定作用范围。
底层原理: Controller 抛异常后,DispatcherServlet 不直接抛给容器,而是遍历 HandlerExceptionResolver 链处理:
ExceptionHandlerExceptionResolver→ 处理 @ExceptionHandler(这是 @ControllerAdvice 生效的关键)ResponseStatusExceptionResolver→ 处理 @ResponseStatus / ResponseStatusExceptionDefaultHandlerExceptionResolver→ 处理 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,前端无法用状态码判断成败