异常处理:@ExceptionHandler/@ControllerAdvice/@RestControllerAdvice
Service 层抛出的异常不该让容器吐一屏堆栈给用户。Spring MVC 把「异常 → 响应」的翻译工作集中到 @ExceptionHandler 方法上,配合 @ControllerAdvice 即可全局统一处理。
DispatcherServlet 的异常处理链
处理器方法抛异常后,DispatcherServlet 不会直接 500,而是按顺序尝试一组 HandlerExceptionResolver,把异常转成 ModelAndView/响应:
| 解析器 | 处理对象 |
|---|---|
| ExceptionHandlerExceptionResolver | @ExceptionHandler 注解方法(主力) |
| ResponseStatusExceptionResolver | @ResponseStatus 标注的异常、ResponseStatusException |
| DefaultHandlerExceptionResolver | Spring 内建异常 → 标准状态码(如 405/415) |
控制器内局部处理
@ExceptionHandler 写在控制器类里,只对本控制器生效:
@RestController
public class UserApi {
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // 可能抛 UserNotFoundException
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleNotFound(UserNotFoundException e) {
return ResponseEntity.status(404).body(e.getMessage());
}
}
@ExceptionHandler 的方法参数可以声明要接的异常(不写类型则按方法参数推断),返回值支持与普通处理方法相同的类型(@ResponseBody、ResponseEntity、视图名等)。
@ControllerAdvice:全局异常处理
把 @ExceptionHandler 提升到全局,一个类管所有控制器:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArg(IllegalArgumentException e) {
return ResponseEntity.badRequest().body("参数不合法:" + e.getMessage());
}
@ExceptionHandler(Exception.class) // 兜底,避免裸堆栈
public ResponseEntity<String> handleOther(Exception e) {
return ResponseEntity.status(500).body("系统繁忙,请稍后重试");
}
}
@ControllerAdvice 还可用于 @InitBinder、@ModelAttribute 等全局方法(定义所有控制器共享的模型属性/数据绑定),作用范围可以按包用 annotations/basePackages 收窄。
@RestControllerAdvice:专为接口而生
@RestControllerAdvice = @ControllerAdvice + @ResponseBody,方法返回值默认直接写响应体,不用每次包 ResponseEntity(想控制状态码时仍可返回 ResponseEntity):
@RestControllerAdvice
public class ApiExceptionAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + " " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return ApiResponse.fail(400, msg);
}
}
// 前端收到:{"code":400,"message":"name 用户名不能为空; age 年龄不能为空"}
匹配优先级
同一个异常被多个 @ExceptionHandler 命中时按「最具体」原则匹配;多个 @ControllerAdvice 之间可用 @Order 排序,控制器自身的方法优先于全局 advice。精确次序以官方文档为准。
Spring 6 新姿势:ProblemDetail(RFC 7807)
Spring 6 起错误响应可遵循 RFC 7807 标准结构(title/status/detail/instance),方式包括抛 ResponseStatusException、在异常上标注、或继承 ResponseEntityExceptionHandler 复用内建映射。标准形态利于跨语言对接,选择以官方文档为准。
生产建议清单
- 兜底 @ExceptionHandler(Exception.class) 必须存在,但记日志再吐统一文案,别把堆栈发给客户端。
- 业务异常设计成自定义异常(如 BizException),全局只处理它 + 框架异常,别到处写 try/catch。
- 校验异常(MethodArgumentNotValidException)、类型不匹配(MethodArgumentTypeMismatchException)等框架异常单独映射成 400。
一句话:异常处理 = 自定义业务异常(抛出点)+ @RestControllerAdvice(翻译点)+ 兜底 Exception(保底点)。三层搭好,接口永远返回你能预期的形状。