@Valid和@Validated区别

211 阅读6分钟

为了方便理解和构造使用场景,目前假设存在三个实体对象,分别是ProjectDTO(项目)、TeamDTO(团队)和MemberDTO(成员),彼此的关系是,一个项目中存在一个团队,一个团队中存在多个成员,实体类里面的属性虚构,目的是为了举例校验的相关注解。

ProjectDTO(项目)实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProjectDTO {
 
    @NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
    private String id;
 
    @NotBlank
    @Pattern(regexp = "[a-zA-Z0-9]", message = "只允许输入数字和字母")
    private String strValue;
 
    @Min(value = -99, message = "值不能小于-99")
    @Max(value = 100, message = "值不能超过100")
    private Integer intValue;
 
    @Negative(message = "值必须为负数")
    private Integer negativeValue;
 
    @EnumValue(strValues = {"agree", "refuse"})
    private String strEnum;
 
    @EnumValue(intValues = {1983, 1990, 2022})
    private Integer intEnum;
 
    @Valid
    private TeamDTO teamDTO;
 
}

TeamDTO(团队)实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TeamDTO {
 
    @FutureOrPresent(message = "只能输入当前年份或未来的年份")
    private Year nowYear;
 
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @Future(message = "只能是未来的时间")
    private Date futureTime;
 
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @Past(message = "只能是过去的时间")
    private Date pastTime;
 
    @Email(message = "请输入正确的邮箱")
    private String email;
 
    @Valid
    private List<MemberDTO> list;
 
}

MemberDTO(成员)实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDTO {
 
    @NotBlank(message = "姓名不能为空")
    private String name;
 
    @EnumValue(intValues = {0, 1, 2}, message = "性别值非法,0:男,1:女,2:其他")
    private Integer sex;
 
}

1. @Valid和@Validated 对应的maven包不一样

2. @Valid和@Validated中常用的注解

常用注解

注解验证数据的类型备注
Null任意类型参数值必须是 Null
NotNull任意类型参数值必须不是 Null
NotBlank只能作用于字符串字符串不能为 null,而且字符串长度必须大于0,至少包含一个非空字符串
NotEmptyCharSequence Collection Map Array参数值不能为null,且不能为空 (字符串长度必须大于0,空字符串(“ ”)可以通过校验)
Length字符串限制字符串的大小
Range数值限制数值的范围
SizeList限制列表的长度
Email任意类型限制元素必须是电子邮箱
Future日期限制必须是一个将来的日期
Past日期限制只能是过去的日期
Pattern任意类型限制必须符合指定的正则表达式

详细分析@NotNull和@NotBlank和NotEmpty如:

1.String name = null;则注解检查结果:
@NotNull: false
@NotEmpty:false 
@NotBlank:false 

2.String name = "";则注解检查结果:
@NotNull:true
@NotEmpty: false
@NotBlank: false

3.String name = " ";则注解检查结果:
@NotNull: true
@NotEmpty: true
@NotBlank: false

4.String name = "Great answer!";则注解检查结果:
@NotNull: true
@NotEmpty:true
@NotBlank:true

3. @Valid和@Validated区别和对应使用场景

类型ValidValidated可以对参数校验进行分组
嵌套验证可以不可以
使用方式用在方法/构造函数/方法参数和成员属性上用在类/方法和方法参数上, 但不能用于成员属性
分组功能没有

4. @Valid的嵌套校验(校验的对象中引入的其他对象或者List对象的校验)

@Valid
private List<MemberDTO> list;

5. @Validated的分组校验(不同的分组不同的校验策略)

例如: 更新项目信息, 项目id是必传项, 在新增项目时, 项目id可以不传, 新增和更新用的同一实体对象, 这个时候就需要根据不同的分组区分, 不同的分组采用不同的校验策略

@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;

group如何定义: 其实很简单, 就是自己定义一个接口, 这个接口的作用只是用来分组, 自己创建一个接口, 代码如下: 分别表示在新增和更新两种情况, 可以按实际需求在内部添加多个接口

public interface TestValidGroup {
 
    interface Insert {
 
    }
 
    interface Update {
 
    }
}

6. @Validated中的分组校验时@GroupSequence使用(指定字段的校验顺序)

如果不指定校验顺序, 每次校验的顺序不同, 错误提示信息也是不同的, 一些特定的场景要求固定错误顺序, 如: 自动化测试脚本, 每次都需要将返回校验的结果和预期结果比较, 返回的校验结果一直变化就会有问题.

controller层

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);
    }
}

指定校验顺序用到@GroupSequence注解, 这个注解使用在group接口上, 可以针对每一个参数都进行分组, 然后通过该注解去指定顺序, 例如更新, 校验的顺序就是先校验group属于Id.class的字段, 再校验group属于StrValue的字段

public interface TestValidGroup {
 
 
    @GroupSequence(value = {StrValue.class})
    interface Insert {
 
    }
 
    @GroupSequence(value = {Id.class, StrValue.class})
    interface Update {
 
    }
 
    interface Id {
 
    }
 
    interface StrValue {
 
    }
}

注意: 此时不是校验group属于Update.class的字段, 而是校验group属于@GroupSequence的Value中的那些接口(id.class, StrValue.class)的字段, 如下: 正确用法:

@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Id.class})
private String id;

错误用法:

@NotBlank(message = "ID不能为空", groups = {TestValidGroup.Update.class})
private String id;

7. 快速失败机制(单个参数校验失败后, 立马抛出异常, 不再对剩下的参数进行校验)

Validation提供了快速失败的机制

@Configuration
public class ValidConfig {

   @Bean
   public Validator validator() {
       ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
               .configure()
               // 快速失败模式
               .failFast(true)
               .buildValidatorFactory();
       return validatorFactory.getValidator();
   }
}

8. 自定义校验注解,实现特殊的校验逻辑

9. 全局异常处理, 统一返回校验异常信息

项目中一般会针对异常进行统一处理, valid校验失败的异常是MethodArgumentNotValidException, 所以可以拦截此类异常, 进行异常信息的处理, 捕获后的具体逻辑, 自行实现, 代码如下:

@Slf4j
@RestControllerAdvice
public class ExceptionHandlerConfig {
    /**
     * 拦截valid参数校验返回的异常,并转化成基本的返回样式
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public BaseResponse dealMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("this is controller MethodArgumentNotValidException,param valid failed", e);
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
        return BaseResponse.builder().code("-10").msg(message).build();
    }
}

10. @Interface List的使用场景

同一字段在不同的场景下, 需要采用不同的校验规则, 并返回不同的异常信息, 目前有两种方式, 一种是采用@List的方式, 一种是在字段上重复使用同一个注解, 代码如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class BaseDTO {
 
    @NotBlank.List({
            @NotBlank(message = "项目BaseId不能为空", groups = {TestValidGroup.Project.class}),
            @NotBlank(message = "团队BaseId不能为空", groups = {TestValidGroup.Team.class})
    })
    private String baseId;
 
    @Max(value = 10, message = "项目BaseId不能大于10", groups = {TestValidGroup.Project.class})
    @Max(value = 30, message = "团队BaseId不能大于30", groups = {TestValidGroup.Team.class})
    private Integer number;
}

目的是通过指定注解归属于不同的分组来到达区分的效果. controller如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/projectList")
    public BaseResponse projectList(@Validated(value = {TestValidGroup.Project.class}) @RequestBody BaseDTO baseDTO) {
        return new BaseResponse(baseDTO);
    }
 
    @PostMapping("/teamList")
    public BaseResponse projectTeam(@Validated(value = {TestValidGroup.Team.class}) @RequestBody BaseDTO baseDTO) {
        return new BaseResponse(baseDTO);
    }
}

11. @Valid和@Validated组合使用

@Validated和Valid肯定是可以组合使用的,一种是分组,一种是嵌套,单独使用的注意点已经在上面的部分写过,下面简单描述下在Controller代码中的使用,其实很简单,就是在实体类(ProjectDTO)上加@Validated即可,内部的@Valid校验也会生效,代码如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class, Default.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);
 
    }
}