1.@Valid 与 @Validated 的核心区别
@Valid 与 @Validated核心区别总结
注解 | @Valid (JSR 标准) | @Validated (Spring 特有) |
---|---|---|
主要作用场景 | 验证复杂的请求体 (@RequestBody) | 激活简单参数 (@PathVariable, @RequestParam) 的验证 |
触发机制 | 直接触发被注解参数的验证 (及级联) | 类级别注解(非直接作用在参数上),激活整个Controller的方法参数验证 |
常用位置 | 方法参数前(尤其 @RequestBody 后) | Controller 类上 |
验证失败异常 | MethodArgumentNotValidException | ConstraintViolationException |
HTTP 状态码 | 默认 400 (BAD_REQUEST) | 默认 500 (INTERNAL_SERVER_ERROR),需自定义处理 |
级联验证 | 支持(需在嵌套对象字段标记 @Valid) | 不支持级联,需结合 @Valid |
结合代码深度解析
场景 1: 验证请求体 (@RequestBody)
@RestController
@RequestMapping("/api")
public class PersonController {
@PostMapping("/person")
public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
}
}
为什么用 @Valid?
- Person 是复杂对象,包含嵌套属性和约束(如
@NotNull
,@Size
,@Email
)。 @Valid
直接标记person
参数,触发对 Person 及其内部属性(若存在嵌套对象且加了@Valid
)的验证。- 验证失败时,Spring 自动转换为
MethodArgumentNotValidException
并返回 HTTP 400。
场景 2: 验证路径参数/查询参数
@RestController
@RequestMapping("/api")
@Validated // 关键!必须加在类上
public class PersonController {
@GetMapping("/person/{id}")
public ResponseEntity<Integer> getPersonByID(
@Valid // 此处可省略,Spring 5.3+ 只需约束注解
@PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id
) {
return ResponseEntity.ok().body(id);
}
@PutMapping("/person")
public ResponseEntity<String> getPersonByName(
@Valid // 此处可省略
@RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name
) {
return ResponseEntity.ok().body(name);
}
}
为什么用 @Validated?
- 对简单参数(如 Integer id, String name),必须先在 Controller 类上标记
@Validated
。这是 Spring 激活方法参数验证的开关。 - 参数本身的约束(如
@Max
,@Size
)是直接标记在参数上的。 @Valid
在简单参数前是多余的(可省略),真正起作用的其实是@Validated
激活的验证机制。- 验证失败时抛出
ConstraintViolationException
,默认返回 HTTP 500!需全局异常处理返回 400。
重要补充说明
级联验证的限制
@Validated
无法替代@Valid
的级联功能。若需要验证嵌套对象(如 Person 中的 Address 对象),必须在 Person 类中的字段上加@Valid
:
public class Person {
@Valid // 必须加!触发 Address 内部验证
private Address address;
}
分组验证场景
- 当需要按业务场景分组验证时(如创建和更新使用不同规则):
public class User {
@NotNull(groups = CreateGroup.class)
private String username;
@Size(min = 6, groups = {CreateGroup.class, UpdateGroup.class})
private String password;
}
// Controller 用法
@PostMapping("/users")
public void createUser(@Validated(CreateGroup.class) @RequestBody User user) {
...
}
- 此时必须用
@Validated(CreateGroup.class)
,因为@Valid
不支持分组。
最终结论 (Spring MVC 场景)
- 验证复杂请求体 → 直接在参数前加
@Valid
@PostMapping(...)
public Result save(@Valid @RequestBody DTO dto) {
...
}
- 验证路径/查询参数 → 在 Controller 类上加
@Validated
@RestController
@Validated // 必须加在类上!
public class MyController {
@GetMapping("/{id}")
public Result get(@PathVariable @Min(1) Integer id) {
...
}
}
-
需要验证分组 → 参数前用
@Validated(分组类)
-
需要级联嵌套对象 → 在实体类字段上加
@Valid
public class Order {
@Valid // 级联验证 Customer
private Customer customer;
}
验证路径参数失败时,务必全局处理 ConstraintViolationException,避免返回 500:
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<?> handleConstraintViolation() {
return ResponseEntity.status(400).body("参数错误");
}
验证配置优化(application.properties)
# 关闭快速失败模式(默认检测所有错误)
spring.mvc.validator.fail-fast=false
# 自定义消息文件
spring.messages.basename=messages/validation
常见错误解决方案
参数验证没有生效?
- 检查是否在 Controller 类上添加了
@Validated
嵌套对象内的验证没触发?
- 确保在嵌套对象字段上添加了
@Valid
分组验证不生效?
- 用
@Validated(Group.class)
替换@Valid
- 检查约束注解是否指定了正确的组:
@NotNull(groups = CreateGroup.class)
参数验证返回 500 错误?
- 添加全局异常处理器,将
ConstraintViolationException
转换为 400 响应
自定义消息不显示?
- 确保在资源文件中配置了对应 key:
NotBlank.person.name=名字不能为空
结论
在 Spring MVC 中:
- 用
@Valid
处理复杂对象请求体(@RequestBody
) - 用
@Validated
激活对简单类型参数(@PathVariable
,@RequestParam
)的验证 - 使用
@Validated
实现分组验证 - 级联验证必须通过
@Valid
实现(嵌套对象上) - 两种验证方式都需要配合 Bean Validation 约束注解使用
2.分组验证(Group Validation)深度解析
分组验证是 Bean Validation(JSR 380)规范的核心功能,它解决了同一个实体在不同业务场景需要不同验证规则的问题。简单来说,它允许您根据当前操作类型(如创建、更新、特定状态修改)动态启用/禁用某些验证规则。
为什么需要分组验证?
考虑一个用户注册/更新场景:
public class User {
@NotNull // ID在创建时应该为空,更新时不能为空
private Long id;
@Size(min=6, max=20) // 创建时必须符合
private String password;
@Email // 创建和更新都需要验证
private String email;
}
没有分组时,会遇到矛盾:
- 创建用户:id 应为空,但
@NotNull
要求不能为空 - 更新用户:password 可能不需要修改,但
@Size
要求必须验证
分组验证如何实现?
步骤 1:定义分组接口(标记接口)
// 创建操作的分组
public interface CreateGroup {}
// 更新操作的分组
public interface UpdateGroup {}
// 密码修改操作的分组
public interface PasswordChangeGroup {}
步骤 2:在实体中绑定分组
public class User {
@NotNull(groups = UpdateGroup.class) // ↑ 只在更新时验证ID
private Long id;
@Null(groups = CreateGroup.class) // ↑ 创建时ID必须为空
private Long id;
@Size(min=6, max=20, groups = {CreateGroup.class, PasswordChangeGroup.class}) // ↑ 创建和修改密码时验证
private String password;
@Email(groups = {CreateGroup.class, UpdateGroup.class}) // ↑ 创建和更新时都验证
private String email;
}
步骤 3:在 Controller 中使用分组
@RestController
public class UserController {
// 创建用户 → 使用 CreateGroup 规则
@PostMapping("/users")
public void createUser(@Validated(CreateGroup.class) @RequestBody User user) {
// 此时验证:id=null + password长度 + email格式
}
// 更新用户 → 使用 UpdateGroup 规则
@PutMapping("/users/{id}")
public void updateUser(@PathVariable Long id, @Validated(UpdateGroup.class) @RequestBody User user) {
// 此时验证:id不为null + email格式
// password 不验证!
}
// 修改密码 → 使用特定分组
@PostMapping("/users/password")
public void changePassword(@Validated(PasswordChangeGroup.class) @RequestBody User user) {
// 只验证 password 长度
}
}
核心特性详解
1. 分组继承
public interface FullUpdate extends UpdateGroup, PasswordChangeGroup {}
Controller 中使用:
@Validated(FullUpdate.class) // 激活两个组的约束
2. 默认分组 (javax.validation.groups.Default
)
未指定 groups
的约束自动属于 Default
组:
@Email // 等价于 @Email(groups = Default.class)
private String email;
3. 分组序列(按顺序验证)
@GroupSequence({BasicCheck.class, AdvancedCheck.class})
public interface CompleteValidation {}
public class User {
@NotBlank(groups = BasicCheck.class)
private String name;
@Email(groups = AdvancedCheck.class)
private String email;
}
使用:@Validated(CompleteValidation.class)
验证顺序:先检查 BasicCheck
组规则,通过后再检查 AdvancedCheck
使用注意事项
@Valid
不支持分组
必须使用 Spring 的 @Validated
注解才能指定分组。
级联验证的分组传递
public class Order {
@Validated(UpdateGroup.class) // 错误!不支持
private User user;
@Valid // 正确:级联验证使用相同的激活分组
private Address address;
}
默认组的特殊行为
@Validated(UpdateGroup.class) // 只验证 UpdateGroup 组的约束
public void update(...) { ... }
如果需要同时验证 Default 组:
public interface UpdateWithDefault extends Default, UpdateGroup {}
最佳实践场景
场景 | 分组方案 |
---|---|
用户注册 | CreateGroup (验证用户名/密码/邮箱) |
用户资料更新 | UpdateGroup (验证邮箱/手机号) |
订单提交 | OrderSubmitGroup (验证地址/支付方式) |
管理员操作 | AdminEditGroup (添加权限验证) |
分步骤表单 | @GroupSequence 定义验证顺序 |
调试技巧
当分组验证未生效时检查:
- 确认
@Validated
注解在类/方法上 - 检查约束注解是否正确指定
groups
属性 - 查看分组接口是否被正确导入
使用调试器查看激活的验证组:
// 在 ConstraintValidator 中获取当前分组
public boolean isValid(Object value, ConstraintValidatorContext context) {
Set<Class<?>> groups = context.getConstraintDescriptor()
.getGroups();
}
结论
分组验证让您的验证逻辑从 “一刀切” 变为 “精准管控”,大幅提升系统的灵活性和可维护性。