ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

@Valid和@Validated详解

2022-07-11 12:35:30  阅读:309  来源: 互联网

标签:校验 value public Valid private 注解 Validated class 详解


在实际的项目开发中,经常会遇到对参数进行校验的场景,最常见的就是后端需要对前端传过来的数据进行校验。

我理解的数据校验大致分为两类:

一类是对数据本身进行校验,不涉及与数据库交互的,比如正则校验、非空校验、指定的枚举数据、最大值、最小值等等。

二类是数据的校验需要和数据库交互的,比如是否唯一(数据库中是否存在)、数量限制(数据库中只能允许存在10条数据)等等。

由于第二类其实属于业务逻辑,这里不做讨论,本文主要是针对第一类场景的数据校验。

其实也可以在业务代码中去做校验判断,但是这样就不够优雅了不是吗,话不多说直接开始正文

按如下目录进行讲述(点击可以直接定位到感兴趣的章节)

1、@Valid和@Validated介绍以及对应的Maven坐标

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

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

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

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

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

7、快速失败机制(单个参数校验失败后,不再对剩下的参数进行校验)

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

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

1、@Valid和@Validated介绍以及对应的Maven坐标

 @Valid和@Validated主要是用于表单校验

Maven一般是跟随spring-boot-starter-parent,也可以自行选择对应的版本,目前spring-boot-starter-validation最新的版本是2.7.0,Maven中心

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
   <version>2.3.7.RELEASE</version>
</dependency>

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

常用的注解如下图,可能由于版本不同略有出入,具体的含义可以看注解上的注释,下面提供了一份整理的注解含义

@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Future 限制必须是一个将来的日期
@FutureOrPresent 未来或当前的日期,此处的present概念是相对于使用约束的类型定义的。例如校验的参数为Year year = Year.now();此时约束是一年,那么“当前”将表示当前的整个年份。
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Negative 绝对的负数,不能包含零,空元素有效可以校验通过
@NegativeOrZero 包含负数和零,空元素有效可以校验通过
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotNull 限制必须不为null
@Null 限制只能为null
@Past 限制必须是一个过去的日期
@PastOrPresent 过去或者当前时间,和@FutureOrPresent类似
@Pattern(value) 限制必须符合指定的正则表达式
@Positive 绝对的正数,不能包含零,空元素有效可以校验通过
@PositiveOrZero 包含正数和零,空元素有效可以校验通过
@Size(max,min) 限制字符长度必须在min到max之间

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

@Valid可以实现嵌套校验,对于对象中引用了其他的对象,依然可以校验

@Validated可以对参数校验进行分组,例如一个对象里面有一个字段id,id在新增数据时可以为空,但是在更新数据时不能为空,此时就需要用到校验分组

具体的使用见下面的章节

为了方便理解和构造使用场景,目前假设存在三个实体对象,分别是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;

}

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

userInfo方法则是采用@Valid方式进行校验,传入的对象时TeamDTO(团队),实体类里面具体的参数可以看第3部分里面实体类的具体代码TeamDTO(团队)实体类

@RestController
@RequestMapping("/valid")
public class TestValidController {
    
    @PostMapping("/userInfo")
    public BaseResponse userInfo(@Valid @RequestBody TeamDTO teamDTO) {
        return new BaseResponse(teamDTO);
    }
}

关键点在于TeamDTO里面的属性List<MemberDTO> list,上面加上@Valid注解,如下:

@Valid
private List<MemberDTO> list;

postman测试结果如下:

可以看到list里面,MemberDTO也被校验了,name和sex不合法。

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

例如有一个场景,更新项目信息,项目id是必须要传的,但是在新增项目时,id可以不传,新增和更新用的同一个实体对象,这个时候需要根据不同的分组区分,不同的分组采用不同的校验策略,具体查询ProjectDTO(项目)实体类

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

如上,注解参数中存在一个groups,表示将该参数归为update组,可以指定一个参数属于多个组

Controller的代码如下,@Validated有一个参数值value,可以校验指定分组的属性,下面就是指定校验groups包含TestValidGroup.Update.class的属性,在ProjectDTO中只有id这个属性的groups满足条件,所以只会校验id这个参数。

@RestController
@RequestMapping("/valid")
public class TestValidController {

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

    }
}

group如何自定义,其实很简单,就是自己定义一个接口,这个接口的作用只是用来分组,自己创建一个接口,代码如下:

分别表示在新增和更新两种情况,可以按实际需求在内部添加多个接口

public interface TestValidGroup {

    interface Insert {

    }

    interface Update {

    }
}

注意:未显示指定groups的字段,默认归于javax.validation.groups包下的Default.class(默认组)

@Validated的value不指定组时,只校验Default组的字段

@Validated的value指定组时,只校验属于指定组的字段,属于Default组的字段不会被校验

若想指定组和默认组都被校验,有两种方式:

1、在@Validated的value中加入默认组,如下:

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

2、将指定的Update接口继承Default接口,如下:

public interface TestValidGroup {

    interface Insert {

    }

    interface Update extends Default {

    }

}

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

从上面的Swagger调试截图可以知道,返回的是所有字段的校验结果,所以存在一个问题,那就是多个校验字段之间的顺序如何保证,如果不指定顺序,那么每次校验的顺序就会不同,那个错误提示信息也就不同,一些特殊场景会要求固定错误顺序,例如自动化测试脚本,每次都需要将返回的校验结果和预期结果比较,返回的校验结果一直变化就会有问题。

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的接口上,可以针对每一个参数都进行分组,然后通过该注解去指定顺序,代码如下,例如update时,校验的顺序就是先校验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;

小知识:一个字段上存在多个注解时,例如@Max和@NotBlank,是按注解从上至下的顺序进行校验的。

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

实际情况中,有时候并不需要校验完所有的参数,只要校验失败,立马抛出异常,Validation提供了快速失败的机制,代码如下:

@Configuration
public class ValidConfig {

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

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

有时候会存在一些特殊的校验逻辑,已有的注解并不能满足要求,此时就可以自定义校验注解,自己实现特殊的校验逻辑,一般分为两步,1、自定义一个注解。2、实现该注解的校验逻辑

例子场景:目前想实现一种校验,传入的字符串必须在指定的字符串数组中存在,传入的数字必须在指定的Integer数组中存在,类似于枚举值。

1、自定义一个注解

自定义注解的方式不用多说,主要讲下和校验相关的地方,@Constraint(validatedBy = {EnumValueValidated.class}),这个注解很关键,里面的validatedBy = {EnumValueValidated.class}是指定具体的校验类,

具体的校验逻辑在EnumValueValidated类里面实现。另外就是注解里面的一些属性,例如message、groups、payload和内部的一个@List注解(这个注解的使用场景后面会讲到),这里可以参考validation已有的注解,基本都是很有用的。

然后就是定义自己需要的一些特殊的属性,方便校验,例如下面的注解中就包含了,isRequire、strValues、intValues。

@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Constraint(validatedBy = {EnumValueValidated.class})
public @interface EnumValue {

    /**
     * 是否需要(true:不能为空,false:可以为空)
     */
    boolean isRequire() default false;

    /**
     * 字符串数组
     */
    String[] strValues() default {};

    /**
     * int数组
     */
    int[] intValues() default {};

    /**
     * 枚举类
     */
    Class<?>[] enumClass() default {};

    String message() default "所传参数不在允许的值范围内";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        EnumValue[] value();
    }
}

2、实现该注解的校验逻辑

具体的代码如下,implements ConstraintValidator<EnumValue, Object>,实现两个方法,分别是initialize(初始化方法)和isValid(校验方法),initialize()主要是加载读取注解上的值并赋值给类变量,

isValid()是实现具体的校验逻辑,此处不具体说明,可自行实现。

public class EnumValueValidated implements ConstraintValidator<EnumValue, Object> {
    private boolean isRequire;
    private Set<String> strValues;
    private List<Integer> intValues;

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        strValues = Sets.newHashSet(constraintAnnotation.strValues());
        intValues = Arrays.stream(constraintAnnotation.intValues()).boxed().collect(Collectors.toList());
        isRequire = constraintAnnotation.isRequire();

        //将枚举类的name转小写存入strValues里面,作为校验参数
        Optional.ofNullable(constraintAnnotation.enumClass()).ifPresent(e -> Arrays.stream(e).forEach(
                c -> Arrays.stream(c.getEnumConstants()).forEach(v -> strValues.add(v.toString().toLowerCase()))
        ));
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null && !isRequire) {
            return true;
        }

        if (value instanceof String) {
            return strValues.contains(value);
        }
        if (value instanceof Integer) {
            return intValues.stream().anyMatch(e -> e.equals(value));
        }

        return false;
    }
}

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();
    }
}

9、@Interface List的使用场景

标签:校验,value,public,Valid,private,注解,Validated,class,详解
来源: https://www.cnblogs.com/zhaodalei/p/16377549.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有