异常处理:@ControllerAdvice 与 HandlerExceptionResolver
结论先行:Spring MVC 的统一异常出口是 DispatcherServlet 中的 HandlerExceptionResolver 责任链,默认注册三个:ExceptionHandlerExceptionResolver(识别 @ExceptionHandler,最常用)、ResponseStatusExceptionResolver(识别 @ResponseStatus)、DefaultHandlerExceptionResolver(处理 404/405/参数类型错误等内建 MVC 异常)。工程上"全局统一异常"的标准做法:@RestControllerAdvice + @ExceptionHandler,把业务异常、参数校验异常与兜底异常统一转成统一响应体。
一、三个默认解析器
| 解析器 | 依据 | 说明 |
|---|---|---|
| ExceptionHandlerExceptionResolver | @ExceptionHandler 注解 | 局部(@Controller 内)与全局(@ControllerAdvice);按异常类型/继承匹配 |
| ResponseStatusExceptionResolver | @ResponseStatus | 把异常映射为 HTTP 状态码 |
| DefaultHandlerExceptionResolver | Spring MVC 内建异常 | NoHandlerFoundException→404、方法不支持→405、类型转换失败→400 等 |
二、全局异常处理器代码
@RestControllerAdvice
public class GlobalExceptionHandler {
// 业务异常:返回业务码
@ExceptionHandler(BizException.class)
public Result<Void> biz(BizException e) { return Result.fail(e.getCode(), e.getMessage()); }
// 参数校验异常(@RequestBody 触发 MethodArgumentNotValidException)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> valid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(400, msg);
}
// 兜底异常:记录日志,防止堆栈泄漏给前端
@ExceptionHandler(Exception.class)
public Result<Void> other(Exception e) {
log.error("unhandled exception", e);
return Result.fail(500, "系统繁忙,请稍后再试");
}
}
三、查找顺序与优先级
方法内 try-catch → 所在 @Controller 的 @ExceptionHandler → 全局 @ControllerAdvice → 内建解析器 → 容器错误页/默认 500。多个匹配时取"异常类型最精确"的处理器。
常见追问 / 记忆点
- 追问 1:@ControllerAdvice 与 @RestControllerAdvice 区别?答:后者 = 前者 + @ResponseBody 语义,方法返回值直接序列化 JSON,纯接口项目用它。
- 追问 2:怎么返回真正的 404/400 状态码?答:抛 ResponseStatusException(HttpStatus.NOT_FOUND, ...) 或异常类标 @ResponseStatus;注意 404 默认需开启 throw-exception-if-no-handler-found 类配置。
- 追问 3:多个 @ExceptionHandler 重叠怎么选?答:按异常继承关系取最近的匹配(子类异常优先于父类)。
- 记忆点:"注解异常找 ExceptionHandler 注解方法,全局用 ControllerAdvice,内建异常有兜底解析器;最外层永远要留 Exception 兜底"。