Spring 如何做全局异常处理和参数校验?@ControllerAdvice 是怎么工作的?
简化版
Spring MVC 用 @ControllerAdvice(或 @RestControllerAdvice)+ @ExceptionHandler 做全局异常处理——把所有 Controller 抛出的异常集中到一个类里统一处理,返回统一格式的错误响应,避免每个 Controller 都写 try-catch。参数校验用 JSR-303/380 校验注解(@NotNull、@NotBlank、@Size、@Min 等)配合 @Valid/@Validated——在 Controller 参数上加 @Valid,Spring 自动校验,校验失败抛 MethodArgumentNotValidException,再由全局异常处理器统一捕获返回友好提示。两者配合,实现「校验 + 异常」的统一处理,让 Controller 只专注业务。
详细版
全局异常处理:
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody,返回 JSON
public class GlobalExceptionHandler {
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(400, msg);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result handleBiz(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
// 兜底处理所有异常
@ExceptionHandler(Exception.class)
public Result handleAll(Exception e) {
log.error("未处理异常", e);
return Result.fail(500, "系统繁忙");
}
}
参数校验:
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String name;
@Min(value = 18, message = "年龄必须≥18")
private Integer age;
@Email(message = "邮箱格式不对")
private String email;
}
@PostMapping("/user")
public Result create(@Valid @RequestBody UserDTO dto) { // @Valid 触发校验
// 校验通过才会进来;不通过抛 MethodArgumentNotValidException,被全局处理器捕获
return Result.ok();
}
@Valid vs @Validated:
| @Valid(JSR-303 标准) | @Validated(Spring 提供) | |
|---|---|---|
| 来源 | Java 标准(javax/jakarta) | Spring |
| 分组校验 | 不支持 | 支持(groups 属性) |
| 用在方法参数级校验 | 支持 | 支持(还支持类级、方法级) |
| 嵌套校验 | 支持(配合字段上的 @Valid) | 需配合 @Valid |
⚠️
@ControllerAdvice底层也是 AOP 思想——它通过ExceptionHandlerExceptionResolver(Spring MVC 的异常解析器)工作:当 Controller 抛异常,DispatcherServlet 找到匹配的@ExceptionHandler方法来处理。注意它只能捕获进入了 DispatcherServlet 的异常(Controller 层及以下),拦截器 preHandle 里抛的、或 Filter 里抛的异常它捕获不到(那些在 SpringMVC 处理链之外)。
完整版教学
一、为什么要全局异常处理
没有全局处理时,每个 Controller 都要自己 try-catch,返回格式各不相同:
// 反面:每个接口都 try-catch,重复、格式混乱
@GetMapping("/user/{id}")
public Object getUser(@PathVariable Long id) {
try {
return userService.getById(id);
} catch (BusinessException e) {
return "{\"code\":" + e.getCode() + ",\"msg\":\"" + e.getMessage() + "\"}";
} catch (Exception e) {
return "{\"code\":500,\"msg\":\"系统错误\"}";
}
}
问题:① 大量重复的 try-catch;② 错误响应格式不统一(有的返回 msg、有的返回 message,前端难处理);③ 业务代码被异常处理淹没。全局异常处理解决这些——把异常处理从每个 Controller 抽出来,集中到一个类统一处理,Controller 只管抛异常、不管怎么处理,返回格式也统一。这符合「关注点分离」——业务逻辑和异常处理各管各的。
二、@ControllerAdvice 的工作原理
@ControllerAdvice 是一个「增强所有 Controller」的组件,配合 @ExceptionHandler 实现全局异常捕获:
请求处理流程中异常的流转:
Controller 方法抛异常
→ DispatcherServlet 捕获到异常
→ 交给异常解析器 ExceptionHandlerExceptionResolver
→ 它在所有 @ControllerAdvice 类里找 @ExceptionHandler(该异常类型) 的方法
→ 找到最匹配的(按异常类型的继承关系,最具体的优先)→ 调用它处理
→ 返回处理结果作为响应
关键机制:按异常类型精确匹配——@ExceptionHandler(BusinessException.class) 只处理 BusinessException,@ExceptionHandler(Exception.class) 兜底处理所有。匹配时优先选最具体的类型(如同时有 BusinessException 和 Exception 的处理器,抛 BusinessException 时选前者)。@RestControllerAdvice = @ControllerAdvice + @ResponseBody,处理结果直接序列化成 JSON(做 REST API 用它)。
三、异常处理器的匹配与优先级
多个 @ExceptionHandler 时,Spring 按「异常类型的匹配精确度」选择:
定义了三个处理器:
@ExceptionHandler(MethodArgumentNotValidException.class) // 最具体
@ExceptionHandler(RuntimeException.class) // 中间
@ExceptionHandler(Exception.class) // 最兜底
抛 MethodArgumentNotValidException 时:
三个都能处理(它是 RuntimeException 也是 Exception 的子类)
→ 选最具体的:MethodArgumentNotValidException 的处理器 ✓
抛一个 IOException 时:
只有 Exception 处理器匹配 → 用它兜底
设计原则:具体异常单独处理(给用户精确提示),最后用 Exception.class 兜底(防止漏网异常直接抛给前端)。兜底处理器要打日志(记录堆栈)+ 返回友好的通用错误(不暴露内部细节如堆栈、SQL——那是安全隐患)。这样既能对已知异常给精确提示,又能兜住未知异常不裸奔。
四、参数校验:JSR-303 校验注解
参数校验用 JSR-303/380(Bean Validation) 标准注解,声明式地在字段上标注约束:
| 注解 | 约束 |
|---|---|
@NotNull | 不为 null |
@NotBlank | 字符串不为空且不全是空格(只用于 String) |
@NotEmpty | 集合/字符串不为空 |
@Size(min, max) | 长度/大小范围 |
@Min/@Max | 数值范围 |
@Email | 邮箱格式 |
@Pattern(regexp) | 正则匹配 |
在 Controller 参数上加 @Valid(或 @Validated)触发校验:
@PostMapping("/user")
public Result create(@Valid @RequestBody UserDTO dto) { ... }
原理:Spring 通过一个 HandlerMethodArgumentResolver(参数解析器),在把请求数据绑定到 UserDTO 后,检测到 @Valid 就调用校验器(如 Hibernate Validator)逐个检查字段约束。校验失败抛 MethodArgumentNotValidException(@RequestBody 场景)或 BindException(表单场景),再由全局异常处理器统一捕获返回友好提示。这样校验逻辑从 Controller 里彻底移除——不用写一堆 if (name == null) return error。
五、@Valid 与 @Validated 的区别
两个触发校验的注解,容易混:
// @Valid:JSR-303 标准注解,支持嵌套校验
public class OrderDTO {
@Valid // 嵌套校验:也校验 user 内部的字段
private UserDTO user;
@NotNull private Long amount;
}
// @Validated:Spring 提供,额外支持"分组校验"
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserDTO {
@NotNull(groups = UpdateGroup.class) // 只在 Update 分组时校验
private Long id;
@NotBlank(groups = CreateGroup.class) // 只在 Create 分组时校验
private String name;
}
@PostMapping
public Result create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) { ... }
核心区别:@Validated 支持分组校验(groups)——同一个 DTO 在「新增」和「修改」场景校验规则不同(新增不需要 id,修改需要 id),用分组区分。而 @Valid 是 Java 标准、不支持分组,但支持嵌套校验(字段上加 @Valid 会级联校验对象内部字段)。实践:一般场景用 @Valid 即可,需要「同一 DTO 分场景校验」时用 @Validated 分组。
六、全局处理的能力边界
@ControllerAdvice 很强,但有能力边界,这是高频追问点:
能捕获:Controller 方法及其调用的 Service/Dao 抛出的异常
(因为这些都在 DispatcherServlet 的处理范围内)
捕获不到:
① Filter 里抛的异常(Filter 在 DispatcherServlet 之前,异常没进 SpringMVC)
② 拦截器 preHandle 里抛的异常(部分场景,取决于时机)
③ 异步线程(@Async)里抛的异常(不在请求线程的调用栈里)
④ 静态资源、404(默认不走 @ExceptionHandler,需额外配置)
原因:@ControllerAdvice 靠 ExceptionHandlerExceptionResolver 工作,它只处理进入了 DispatcherServlet 处理链的异常。Filter 层的异常发生在 SpringMVC 之外,管不到——那些要用 Filter 自己的 try-catch 或配置 error-page。理解这个边界,才能答准「为什么我的全局异常处理器没捕获到某个异常」——十有八九是异常发生在 SpringMVC 处理链之外(Filter、异步线程)。
记忆钩子:「@RestControllerAdvice + @ExceptionHandler 全局异常(按异常类型精确匹配、Exception 兜底、别暴露堆栈);@Valid/@Validated + JSR-303 注解参数校验(校验失败抛 MethodArgumentNotValidException 被统一捕获);@Validated 支持分组、@Valid 支持嵌套;只能捕获 DispatcherServlet 处理链内的异常,Filter/异步的管不到」。
七、常见误区与追问
- 误区:@ControllerAdvice 能捕获所有异常。 只能捕获进入 DispatcherServlet 处理链的异常(Controller 及以下);Filter 里、异步线程里抛的异常捕获不到。
- 误区:@Valid 和 @Validated 完全一样。 @Validated 是 Spring 的、支持分组校验(groups);@Valid 是 Java 标准、不支持分组但支持嵌套校验(字段上 @Valid 级联校验)。
- 误区:参数校验要在 Controller 里手写 if 判断。 用 JSR-303 注解 + @Valid 声明式校验,校验失败自动抛异常被全局处理器捕获,Controller 无需手写校验逻辑。
- 误区:全局异常处理器直接把异常信息返回给前端。 兜底处理器应打日志记录堆栈,但只返回友好的通用提示,不能暴露堆栈/SQL 等内部细节(安全隐患)。
- 追问:@ExceptionHandler 匹配多个异常类型时选哪个? 选最具体的(按异常继承关系)——如同时有 BusinessException 和 Exception 处理器,抛 BusinessException 时用前者,最具体优先。
- 追问:@RestControllerAdvice 和 @ControllerAdvice 的区别? @RestControllerAdvice = @ControllerAdvice + @ResponseBody,处理结果直接序列化成 JSON 返回,适合 REST API;@ControllerAdvice 默认返回视图。
- 追问:@RequestBody 校验失败抛什么异常?表单呢? @RequestBody(JSON)校验失败抛 MethodArgumentNotValidException;表单/@ModelAttribute 校验失败抛 BindException,都可在全局处理器里捕获。
八、加强记忆
Spring MVC 用 @RestControllerAdvice + @ExceptionHandler 做全局异常处理——把所有 Controller 的异常集中到一个类统一处理、返回统一格式,避免每个接口写 try-catch。底层靠 ExceptionHandlerExceptionResolver:Controller 抛异常后 DispatcherServlet 找匹配的 @ExceptionHandler 方法,按异常类型最具体优先匹配,用 Exception.class 兜底(打日志但别暴露堆栈)。参数校验用 JSR-303 注解(@NotBlank/@Min/@Email 等)+ @Valid/@Validated 声明式触发,校验失败抛 MethodArgumentNotValidException(@RequestBody)被全局处理器统一捕获,Controller 无需手写 if 校验。@Valid(Java 标准)支持嵌套校验、@Validated(Spring)额外支持分组校验(同一 DTO 分新增/修改场景)。关键边界:@ControllerAdvice 只能捕获进入 DispatcherServlet 处理链的异常,Filter 层、@Async 异步线程里的异常管不到。一句话「@RestControllerAdvice+@ExceptionHandler 全局异常(最具体优先、Exception 兜底)、@Valid/@Validated+JSR303 声明式校验、只捕获 SpringMVC 处理链内异常」。