Spring 中 @Valid 与 @Validated 的核心区别以及分组验证(Group Validation)深度解析

1.@Valid 与 @Validated 的核心区别

@Valid 与 @Validated核心区别总结

注解@Valid (JSR 标准)@Validated (Spring 特有)
主要作用场景验证复杂的请求体 (@RequestBody)激活简单参数 (@PathVariable, @RequestParam) 的验证
触发机制直接触发被注解参数的验证 (及级联)类级别注解(非直接作用在参数上),激活整个Controller的方法参数验证
常用位置方法参数前(尤其 @RequestBody 后)Controller 类上
验证失败异常MethodArgumentNotValidExceptionConstraintViolationException
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();
}

结论

分组验证让您的验证逻辑从 “一刀切” 变为 “精准管控”,大幅提升系统的灵活性和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值