Java编程高频的“踩坑点”-02:@Valid对嵌套对象的属性校验应用
一、Valid对嵌套对象的属性校验应用jakarta.validation.Valid是 Jakarta Bean ValidationJSR-380规范中的一个核心注解它的主要作用是触发对对象属性的校验。在实际开发中它主要有以下三个核心使用场景1. Controller 层接口参数校验最常见这是Valid最典型的使用场景。当我们在 Spring Boot 的 Controller 层接收前端传来的 JSON 数据通常使用RequestBody绑定时在参数对象前加上ValidSpring MVC 就会自动触发对该对象内部字段的校验。作用如果对象内部的字段不符合校验规则如NotBlank,Min等Spring 会自动抛出MethodArgumentNotValidException异常阻止业务代码继续执行。代码示例RestController public class UserController { PostMapping(/users) public ResponseEntityString 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的核心区别在实际项目中经常会看到这两个注解混用它们虽然功能相似但有一些关键区别表格特性ValidValidated来源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用法也是一样的。例如ListValid AddressDTO或者直接在字段上加Valid分组转换和传递的规则完全一致。异常处理完全兼容你之前配置的全局异常处理器拦截MethodArgumentNotValidException依然完美适用。嵌套对象校验失败时返回的错误信息中field字段会自动带上层级路径例如address.detail前端可以直接根据这个路径定位到具体的报错输入框。总结建议在实际开发中建议优先使用方案一 (ConvertGroup)因为它语义最清晰能够精确控制外层分组与内层分组的对应关系避免因为默认分组传递机制的不确定性导致校验失效。