(一)、@Valid对嵌套对象的属性校验应用
jakarta.validation.Valid是 Jakarta Bean Validation(JSR-380)规范中的一个核心注解,它的主要作用是触发对对象属性的校验。在实际开发中,它主要有以下三个核心使用场景:
1. Controller 层接口参数校验(最常见)
这是@Valid最典型的使用场景。当我们在 Spring Boot 的 Controller 层接收前端传来的 JSON 数据(通常使用@RequestBody绑定)时,在参数对象前加上@Valid,Spring MVC 就会自动触发对该对象内部字段的校验。
- 作用:如果对象内部的字段不符合校验规则(如
@NotBlank,@Min等),Spring 会自动抛出MethodArgumentNotValidException异常,阻止业务代码继续执行。 - 代码示例:
@RestController public class UserController { @PostMapping("/users") public ResponseEntity<String> createUser (@Valid @RequestBody UserDTO userDTO) { // 只有当 userDTO 内部字段校验全部通过, // 才会执行到这里 return ResponseEntity.ok("用户创建成功"); } }2. 嵌套对象的级联校验
当一个对象内部包含了另一个自定义对象作为属性时,如果希望校验逻辑能深入到这个嵌套对象内部,就需要在嵌套对象的属性上加上@Valid。
- 作用:触发递归校验。它不仅会校验当前对象的基本字段,还会“钻进去”校验嵌套对象里的字段规则。
- 代码示例:
public class OrderDTO { @NotBlank(message = "订单号不能为空") private String orderNo; // 触发对 UserDTO 内部字段 // (如 username, email)的校验 @Valid private UserDTO user; }3. Service 层方法参数校验(需配合特定配置)
虽然@Valid可以写在 Service 层的方法参数上,但在 Spring 中默认是不会自动生效的。如果希望在 Service 层也进行参数校验,通常需要配合 Spring 提供的@Validated注解来开启方法级别的校验支持。
- 作用:明确告诉开发者该参数必须是经过校验的,起到文档化和防御性编程的作用。
- 代码示例:
// 必须在类上添加此注解, // 才能让方法参数的 @Valid 生效 @Service @Validated public class UserService { public void createUser(@Valid UserDTO userDTO) { // 业务逻辑... } }💡 延伸补充:@Valid和@Validated的核心区别
在实际项目中,经常会看到这两个注解混用,它们虽然功能相似,但有一些关键区别:
表格
| 特性 | @Valid | @Validated |
|---|---|---|
| 来源 | Jakarta Bean Validation (JSR-380 标准) | Spring Framework (Spring 提供的扩展) |
| 嵌套校验 | ✅ 支持(在字段上标注可触发级联校验) | ✅ 支持 |
| 分组校验 | ❌ 不支持 | ✅ 支持(可以通过指定 group 实现不同场景的差异化校验) |
| 典型场景 | Controller 的@RequestBody参数、嵌套对象字段 | Controller / Service 层的方法参数、分组校验场景 |
最佳实践建议:
- 在 Controller 层接收前端参数时,推荐使用
@Valid(或者@Validated也可以,Spring MVC 对其做了兼容)。 - 如果存在复杂的嵌套对象需要校验,务必在嵌套的字段上加上
@Valid。 - 如果需要根据不同业务场景(比如“新增”和“修改”时对同一个字段的要求不同)进行分组校验,必须使用
@Validated。
(二)、分组校验,区分校验规则
在 Spring Boot 中,实现分组校验(Validation Groups)是解决“同一个 DTO 在新增和修改时校验规则不同”的最佳方案。
这通常分为三个标准步骤:定义分组接口->在 DTO 字段上分配分组->在 Controller 中触发指定分组。
以下是具体的实现流程:
🛠️ 第一步:定义分组接口(空接口作为标记)
首先,需要定义两个空的接口,分别代表“新增”和“修改”两种场景。这两个接口仅作为逻辑上的分组标签。
import jakarta.validation.groups.Default; public class ValidGroups { // 新增分组 public interface Create extends Default { } // 修改分组 public interface Update extends Default { } }💡 避坑指南(Default 分组问题):
如果你希望某些字段(如username)在所有场景下都生效,或者希望在触发Create分组时也能自动触发那些没有指定分组的默认校验规则,可以让你的分组接口继承jakarta.validation.groups.Default。
📝 第二步:在 DTO 字段上分配分组规则
在你的实体类(DTO)中,通过校验注解的groups属性,将不同的校验规则分配给不同的分组。
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; public class UserDTO { // 新增时 ID 必须为空;修改时 ID 必须非空 @Null(message = "新增时ID必须为空", groups = ValidGroups.Create.class) @NotNull(message = "修改时ID不能为空", groups = ValidGroups.Update.class) private Long id; // 无论是新增还是修改,用户名都不能为空 // 这里将默认规则同时分配给两个分组(或者让分组继承 Default) @NotBlank(message = "用户名不能为空", groups = {ValidGroups.Create.class, ValidGroups.Update.class}) private String username; // 密码仅在新增时必填,修改时可选 @NotBlank(message = "新增时密码不能为空", groups = ValidGroups.Create.class) private String password; }🎯 第三步:在 Controller 中触发指定分组
在 Controller 的方法参数上,必须使用 Spring 提供的@Validated注解(而不是标准的@Valid),并指定当前接口需要触发的分组。
import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/users") public class UserController { // 新增接口 -> 触发 Create 分组校验 @PostMapping public String createUser(@Validated(ValidationGroups.Create.class) @RequestBody UserDTO userDTO) { // 校验通过,执行新增逻辑 return "用户新增成功"; } // 修改接口 -> 触发 Update 分组校验 @PutMapping public String updateUser(@Validated(ValidationGroups.Update.class) @RequestBody UserDTO userDTO) { // 校验通过,执行修改逻辑 return "用户修改成功"; } }💡 核心原理与注意事项
为什么必须用
@Validated?
标准的@Valid注解(Jakarta Bean Validation 规范)不支持分组功能,它只会无差别地触发对象上所有的校验规则。而Spring 提供的@Validated扩展了该功能,允许通过指定groups参数来按需触发校验。异常处理兼容性:
你之前配置的全局异常处理器(@RestControllerAdvice拦截MethodArgumentNotValidException)在这里依然完全适用。当指定分组的校验失败时,Spring 依然会抛出该异常,你的统一异常处理逻辑不需要做任何修改即可完美兼容分组校验。嵌套对象的分组校验:
如果你的 DTO 内部包含了其他自定义对象(嵌套对象),并且希望嵌套对象也遵循同样的分组规则,需要在嵌套字段上同时加上@Valid和对应的分组配置(Spring Boot 3.x 对嵌套对象的分组传递有特定要求,通常建议在嵌套字段上也显式指定分组或使用@ConvertGroup)。
(三)、嵌套对象下分组校验失效
分组校验怎么配合嵌套对象使用?
在 Spring Boot 中,将分组校验与嵌套对象结合使用时,有一个非常关键且容易踩坑的机制:校验分组默认不会自动传递给嵌套对象。
这意味着,如果你在 Controller 中指定了@Validated(Create.class),这个Create分组只会作用于最外层的 DTO 对象。如果嵌套对象内部的字段也定义了分组规则,它们默认是不会被触发的。
要解决这个问题,主要有两种成熟的方案:
🛠️ 方案一:使用@ConvertGroup进行分组转换(最推荐)
如果你希望外层触发Create分组时,嵌套对象也能自动触发它对应的Create分组规则,可以使用 Bean Validation 提供的@ConvertGroup注解。它的作用是建立一种“映射关系”,将外层的分组动态转换为内层需要的分组。
import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.groups.ConvertGroup; public class UserDTO { @NotBlank(message = "用户名不能为空", groups = ValidationGroups.Create.class) private String username; // 核心:将外层的 Create 分组, // 转换为内层 AddressDTO 需要的 Create 分组 @Valid @ConvertGroup(from = ValidationGroups.Create.class, to = ValidationGroups.Create.class) private AddressDTO address; } public class AddressDTO { // 只有在接收到 Create 分组时,这个规则才会生效 @NotBlank(message = "详细地址不能为空", groups = ValidationGroups.Create.class) private String detail; }💡 方案二:嵌套字段不指定分组(适用于通用规则)
如果嵌套对象内部的校验规则是“通用”的(即无论是新增还是修改,这些字段都必须校验),那么你在定义嵌套对象的 DTO 时,不要给这些字段的校验注解指定groups属性。
不指定groups的规则默认属于Default分组。在 Spring 的校验机制中,只要嵌套对象被@Valid触发,属于Default分组的规则通常会被一并执行。
public class UserDTO { @Valid // 只要加上 @Valid,就会递归校验 AddressDTO private AddressDTO address; } public class AddressDTO { // 没有指定 groups,属于 Default 分组 // 无论外层触发 Create 还是 Update,这个字段都会被校验 @NotBlank(message = "城市不能为空") private String city; }⚠️ 核心注意事项与避坑指南
@Valid必不可少:无论使用哪种方案,在嵌套对象的字段上必须加上@Valid注解,否则 Spring 根本不会进入该对象内部进行校验。- 集合/数组的嵌套校验:如果你的嵌套对象是
List或Map,用法也是一样的。例如List<@Valid AddressDTO>或者直接在字段上加@Valid,分组转换和传递的规则完全一致。 - 异常处理完全兼容:你之前配置的全局异常处理器(拦截
MethodArgumentNotValidException)依然完美适用。嵌套对象校验失败时,返回的错误信息中,field字段会自动带上层级路径(例如address.detail),前端可以直接根据这个路径定位到具体的报错输入框。
总结建议:
在实际开发中,建议优先使用方案一 (@ConvertGroup),因为它语义最清晰,能够精确控制外层分组与内层分组的对应关系,避免因为默认分组传递机制的不确定性导致校验失效。