SpringBoot 三大核心注解精讲:@ControllerAdvice、@RestControllerAdvice、@Validated 分组校验(实战)

在 SpringBoot 前后端分离开发中,全局统一异常处理参数分组校验是项目标准化的核心环节。

绝大多数新手和初级开发者都会遇到这几个问题:

  • 全局异常拦截不住 Assert 断言、参数校验异常

  • 新增/修改复用 DTO 时,校验规则冲突无法解决

  • 分不清 @ControllerAdvice@RestControllerAdvice 的区别

  • 错误使用 Map 接收参数,导致校验完全失效

本文讲解三个高频核心注解,总结及落地。


一、全局增强注解:@ControllerAdvice 与 @RestControllerAdvice

1. 核心区别

两者都是 Spring 提供的 Controller 全局增强器,用于统一处理控制器层的异常、参数绑定、全局数据填充。

注解 底层组合 返回值场景 项目适用场景
@ControllerAdvice 原生增强注解 默认返回视图页面,返回 JSON 需要手动加 @ResponseBody 传统 Web 页面项目
@RestControllerAdvice @ControllerAdvice + @ResponseBody 所有方法自动序列化为 JSON 前后端分离项目(首选)

2. 三大核心能力

两个注解功能完全一致,仅返回值格式不同,支持三大能力:

  1. @ExceptionHandler:全局异常捕获(核心常用)

  2. @InitBinder:表单参数自定义绑定、格式化

  3. @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 内置机制实现。

相关推荐
吠品1 小时前
Java byte数组与String互转:编码细节与踩坑记录
java·linux·服务器
晚风醉蝶2 小时前
1-6-插入排序-InsertionSort
java·数据结构·排序算法
yaoxin5211232 小时前
497. Java 反射 - 使用反射读取注解
java·开发语言·python
eralong3 小时前
Java 面向对象:继承、多态、接口
java·后端
哦虎!3 小时前
【数据库】事务
java·数据库·mysql
CDN3603 小时前
流媒体加速实践:出海东南亚短视频点播频繁缓冲,HLS 分片与 Nginx 配置调优
java·网络·nginx·流媒体加速
程序员黑豆4 小时前
Java中的null与NullPointerException完全指南:安全处理、实战排查与面试题
java·前端·ai编程
晴天164 小时前
Electron面试题-Day19
java·javascript·electron
GeekZHR4 小时前
C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“
java·c语言·算法·指针