在 SpringBoot 前后端分离开发中,全局统一异常处理 和参数分组校验是项目标准化的核心环节。
绝大多数新手和初级开发者都会遇到这几个问题:
-
全局异常拦截不住 Assert 断言、参数校验异常
-
新增/修改复用 DTO 时,校验规则冲突无法解决
-
分不清
@ControllerAdvice和@RestControllerAdvice的区别 -
错误使用 Map 接收参数,导致校验完全失效
本文讲解三个高频核心注解,总结及落地。
一、全局增强注解:@ControllerAdvice 与 @RestControllerAdvice
1. 核心区别
两者都是 Spring 提供的 Controller 全局增强器,用于统一处理控制器层的异常、参数绑定、全局数据填充。
| 注解 | 底层组合 | 返回值场景 | 项目适用场景 |
|---|---|---|---|
@ControllerAdvice |
原生增强注解 | 默认返回视图页面,返回 JSON 需要手动加 @ResponseBody |
传统 Web 页面项目 |
@RestControllerAdvice |
@ControllerAdvice + @ResponseBody |
所有方法自动序列化为 JSON | 前后端分离项目(首选) |
2. 三大核心能力
两个注解功能完全一致,仅返回值格式不同,支持三大能力:
-
@ExceptionHandler:全局异常捕获(核心常用)
-
@InitBinder:表单参数自定义绑定、格式化
-
@ModelAttribute:全局自动填充公共返回参数
3. 生效范围(避坑)
✅ 可拦截:所有 Controller 层抛出的异常
❌ 无法拦截:
-
Filter 过滤器抛出的异常(直接走 Spring 默认 /error 接口)
-
@Async 异步线程异常(非 Web 主线程)
-
代码中 try-catch 捕获后未重新抛出的异常
4. 带参数校验的完整全局异常处理器
java
/**
* 全局统一异常处理器
* 覆盖:参数校验异常、断言异常、业务异常、系统兜底异常
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* JSON请求体校验异常(@RequestBody + @Validated)
* 前端传JSON参数校验不通过触发
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidException(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String msg = fieldError != null ? fieldError.getDefaultMessage() : "参数非法";
log.warn("JSON参数校验失败:{}", msg);
return buildResult(HttpStatus.BAD_REQUEST.value(), "参数校验失败:" + msg);
}
/**
* 普通参数校验异常(@RequestParam/@PathVariable + @Validated)
* 地址栏参数、表单参数校验不通过触发
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handleBindException(BindException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String msg = fieldError != null ? fieldError.getDefaultMessage() : "参数非法";
log.warn("普通参数校验失败:{}", msg);
return buildResult(HttpStatus.BAD_REQUEST.value(), "参数校验失败:" + msg);
}
/**
* Service层方法参数校验异常
* Service类上加@Validated触发
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleConstraintException(ConstraintViolationException e) {
String msg = e.getConstraintViolations().iterator().next().getMessage();
log.warn("Service参数校验失败:{}", msg);
return buildResult(HttpStatus.BAD_REQUEST.value(), "参数校验失败:" + msg);
}
/**
* 断言异常、参数非法异常(Assert.notNull / 手动参数判断抛出)
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleArgException(IllegalArgumentException e) {
log.warn("参数断言异常:{}", e.getMessage());
return buildResult(HttpStatus.BAD_REQUEST.value(), e.getMessage());
}
/**
* 全局兜底异常(所有未捕获的未知异常)
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleAllException(Exception e) {
log.error("系统未知异常", e);
return buildResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器繁忙,请稍后重试");
}
/**
* 统一封装返回结果,保证全局报文格式一致
*/
private ResponseEntity<Map<String, Object>> buildResult(int code, String msg) {
Map<String, Object> result = new HashMap<>(3);
result.put("code", code);
result.put("msg", msg);
result.put("data", null);
return ResponseEntity.status(code).body(result);
}
}
5. 高级用法:限定生效范围
默认全局生效,可指定包、注解、类,实现局部异常拦截:
java
/**
* @RestControllerAdvice 精准生效范围配置(企业级精细化管控)
* 不配置默认全局生效,大型项目建议精准指定生效范围,避免全局拦截干扰
*/
// 1. 仅拦截指定包下的所有Controller(最常用、推荐)
@RestControllerAdvice(basePackages = "com.xxx.biz.controller")
// 2. 仅拦截带有自定义标记注解的Controller(模块化管控)
// @RestControllerAdvice(annotations = com.xxx.common.annotation.ApiController.class)
// 3. 仅拦截指定的单个/多个Controller(精准定向拦截)
// @RestControllerAdvice(assignableTypes = {com.xxx.biz.controller.UserController.class})
public class GlobalExceptionHandler {
// 异常处理方法...
}
二、@Validated 分组校验:解决新增/修改DTO复用难题
1. 核心作用
@Validated 是 Spring 提供的参数校验注解,相比于 JSR303 原生的 @Valid,唯一核心优势:支持分组校验。
解决痛点:同一个 DTO,新增不需要 ID、修改必须传 ID 的场景,完美复用实体类,无需拆分两个 DTO。
2. @Valid 与 @Validated 终极区别
| 特性 | @Valid | @Validated |
|---|---|---|
| 分组校验 | ❌ 不支持 | ✅ 支持(核心) |
| 使用位置 | 方法参数、字段(嵌套校验) | 类、方法、方法参数 |
| 嵌套校验 | ✅ 支持 | ❌ 字段上不支持 |
| 来源 | JSR303 原生规范 | Spring 扩展注解 |
3. 项目分组方案
很多新手每个 DTO 新建分组接口,导致项目文件爆炸。项目标准方案:公共全局分组 + 局部扩展
第一步:公共模块定义全局通用分组(仅1次,全项目复用)
java
/**
* 全局通用校验分组(项目统一规范,全模块复用)
* 所有业务DTO统一使用该分组,杜绝重复定义
*/
/** 新增操作分组 */
public interface Create {}
/** 修改/更新操作分组 */
public interface Update {}
/** 查询操作分组 */
public interface Query {}
/** 删除操作分组 */
public interface Delete {}
第二步:DTO 绑定分组规则
java
/**
* 用户业务DTO(新增/修改复用,分组校验落地示例)
* 无groups注解默认归属Default分组,通用场景全局生效
*/
public class UserDTO {
/** 修改必填、新增无需校验(仅Update分组生效) */
@NotNull(message = "用户ID不能为空", groups = Update.class)
private Long id;
/** 新增、修改通用必填(多分组复用) */
@NotBlank(message = "用户名不能为空", groups = {Create.class, Update.class})
private String username;
/** 通用字段:无分组,默认所有场景都校验 */
@NotBlank(message = "手机号不能为空")
private String phone;
// getter、setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
}
第三步:Controller 启用分组校验
java
/**
* 分组校验 Controller 落地示例
* 核心要点:需拼接Default分组,让无分组的通用字段生效
*/
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 新增用户
* 只校验Create分组 + 默认通用字段
*/
@PostMapping("/add")
public String add(@Validated({Create.class, javax.validation.groups.Default.class}) @RequestBody UserDTO dto) {
// 新增业务逻辑
return "新增成功";
}
/**
* 修改用户
* 只校验Update分组 + 默认通用字段
*/
@PostMapping("/update")
public String update(@Validated({Update.class, javax.validation.groups.Default.class}) @RequestBody UserDTO dto) {
// 修改业务逻辑
return "修改成功";
}
}
4. 特殊场景:DTO 独有业务分组(局部扩展)
若某个 DTO 有独有校验场景(如订单提交、支付),无需新建文件,直接在 DTO 内部定义静态接口:
java
/**
* 特殊业务DTO:全局分组 + 局部自定义分组结合示例
* 适用于当前DTO独有校验场景,无需新建全局分组文件
*/
public class OrderDTO {
/** 订单独有校验分组(仅当前订单业务使用) */
public interface SubmitOrder {}
/** 修改通用校验(复用全局分组) */
@NotNull(message = "订单ID不能为空", groups = Update.class)
private Long orderId;
/** 订单提交专属校验(局部自定义分组) */
@NotBlank(message = "支付密码不能为空", groups = SubmitOrder.class)
private String payPwd;
// getter、setter
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public String getPayPwd() { return payPwd; }
public void setPayPwd(String payPwd) { this.payPwd = payPwd; }
}
5. 关键避坑:Default 默认分组
未指定 groups 的校验注解,默认属于 Default 分组。
如果需要通用字段全局生效,必须手动拼接 Default 分组:
java
/**
* Default默认分组 正确使用示例
* 只写自定义分组会丢失无注解字段校验,必须拼接Default
*/
@RestController
public class OrderController {
@PostMapping("/order/update")
// 同时生效:自定义Update分组 + 全局Default默认分组
public String updateOrder(@Validated({Update.class, javax.validation.groups.Default.class}) @RequestBody OrderDTO dto) {
return "修改成功";
}
}
三、需注意
1. @Validated 不支持 Map 参数校验
❌ 错误写法(完全不生效):
java
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 致命错误示例:Map参数无法使用@Validated校验
* 原因:JSR303校验基于JavaBean注解,Map无注解标识,校验完全失效
*/
@RestController
public class ErrorController {
@PostMapping("/test")
public String test(@Validated @RequestBody Map<String, Object> map) {
return "测试";
}
}
✅ 正确做法:所有接口统一使用 DTO 接收参数,禁止 Map 接收 JSON 请求体。
2. 嵌套校验必须用 @Valid
嵌套对象字段上,只能用 @Valid,禁止用 @Validated,分组会自动继承上层参数的分组规则。
3. Assert 断言异常拦截问题
Assert.notNull() 抛出 IllegalArgumentException,只要方法异常向上抛出,会被全局异常处理器正常拦截,无需手动 try-catch。
4. 异常拦截优先级
精确异常 > 父类通用异常(例如:先捕获 NullPointerException,再捕获 Exception)
四、总结
1. 注解使用规范
-
前后端分离项目:强制使用 @RestControllerAdvice
-
参数分组校验:优先 @Validated,嵌套字段校验用 @Valid
-
禁止使用 Map 接收 JSON 参数,统一 DTO 规范化校验
2. 分组校验选型规范(项目)
-
通用场景:复用 common 模块全局 Create/Update/Query/Delete 分组
-
特殊场景:DTO 内部自定义静态分组接口,不新建冗余文件
-
绝对禁止:每个 DTO 新建独立分组接口,导致项目文件泛滥
3. 异常处理规范
全局异常必须覆盖 4 类核心异常:参数校验异常、断言异常、空指针异常、全局兜底异常,保证项目返回报文统一、规范、友好。
五、底层原理深度拆解:三大注解核心实现机制
很多开发者只会用、不懂原理,面试高频问点:全局异常怎么拦截的?分组校验底层如何实现?两者依赖什么 Spring 机制?
本节从 AOP 机制、Spring 容器注册、Validator 校验器、异常处理器链 四层拆解底层,吃透本质。
1. @ControllerAdvice / @RestControllerAdvice 底层原理
核心本质:基于 Spring AOP + 全局异常处理器链
它并不是拦截器、不是过滤器,本质是 Spring 全局控制器增强组件,依托 SpringMVC 内置机制实现。