首页 > 其他分享 >spring-boot-@Valid和@Validated详解

spring-boot-@Valid和@Validated详解

时间:2022-12-05 10:11:38浏览次数:58  
标签:return spring boot 校验 value public Valid private class

----------------------------------------------------------------------------------------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

10、@Interface List的使用场景(补充)

11、@Valid和@Validated组合使用(补充)

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

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)上同时加上这两个注解,代码如下:

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

    }
}

----------------------------------------------------------------------------------------

@Validated注解

@Validated注解是为了给请求接口时,判断对象的值是否是你需要的属性做判断。

在编程中我使用的主要用的有:

1、@NotNull (不能为null)

2、@NotEmpty (不为空也不能为null,其主要限制String类型)

3、@NotBlank (不能为空,去除一开始的空格后长度不能为0)

在网上还看到一些常用注释,现记录下来:

感觉比较实用的:

1、@Size(max,min) (限制字符长度必须在min到max之前)

2、@Past (限制必须是过去的日期)

3、@PastOrPresent (限制必须是过去的日期或者是当前时间)

4、@Future (限制必须是以后的日期)

5、@FutureOrPresent (限制必须是以后的日期或者是当前时间)

6、@Max(value) (限制必须为不大于value的值)

7、@Min(value) (限制必须为不小于value的值)

8、@Pattern(value) (限制必须符合指定的正则表达式)

9、@Email (限制必须为email格式)

不常用的:

1、@Null (限制只能为空)

2、@AssertFalse (限制必须是false)

3、@AssertTrue. (限制必须为true)

4、@DecimalMax(value) (限制必须为不大于value的值)

5、@DecimalMin(value) (限制必须为不小于value的值)

6、@Digits(Integer,fraction)(限制必须为一个小数,且整数部分位数不超过Intger,小数不超过fraction)

7、@Negative (限制必须为负整数)

8、@NegativeOrZero (限制必须为负整数或零)

8、@Positive (限制必须为正整数)

9、@PositiveOrZero (限制必须为正整数或零)

现在来讲一下如何使用:

1、导入依赖(有两种)

第一种:(我使用的是第一种)

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
第二种:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、创建对象、接口;

创建对象

@Data
public class XxxAddParam{

@NotNull(message = "用户id不能为空")
private Integer id;

@NotEmpty(message = "用户姓名不能为空")
private String name;
}
创建接口

@Slf4j
@RestController
@RequestMapping("/xxx")
public class XxxController {

@PostMapping("/addXxx")
public Result addXxx(@RequestBody @Validated XxxAddParam XxxAddParam){
return Result.success();
}

}
在一开始可能会遇到如下问题:
1、@Validated注解的默认异常过长;

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.xwdBoy.common.helper.Result com.xwdBoy.web.controller.XxxController.addXxx(com.xwdBoy.param.XxxAddParam): [Field error in object 'XxxAddParam' on field 'name': rejected value []; codes [NotEmpty.XxxAddParam.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [XxxAddParam.name,name]; arguments []; default message [name]]; default message [用户姓名不能为空]]

2、无法对List<xx>对象进行验证;

第一个问题是需要加上全局异常处理就可以通过:
/**
* HTTP接口统一的错误处
* @author sj
*/
@ControllerAdvice
@Slf4j
@Priority(1)
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
List<FieldError> errors = bindingResult.getFieldErrors();
//初始化错误信息大小
Result result = new Result();
for (FieldError error : errors) {
result.setMsg(error.getDefaultMessage());
result.setCode(ResultEnum.ERROR.getCode());
return result;
}
return Result.error(ResultEnum.ERROR.getCode(), ResultEnum.ERROR.getMsg());
}
@ExceptionHandler(BizException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleBizExceptions(BizException e) {
log.error(e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}

@ResponseBody
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result httpRequestMethodNotSupportedExceptionException(HttpRequestMethodNotSupportedException e) {
String message = Optional.ofNullable(e.getMessage()).orElse(ResultEnum.HTTP_REQUEST_METHOD_NOT_SUPPORTED.getMsg());
log.warn("HttpRequestMethodNotSupportedException:", e);
return Result.error(ResultEnum.HTTP_REQUEST_METHOD_NOT_SUPPORTED.getCode(), message);
}

@ResponseBody
@ExceptionHandler(MultipartException.class)
public Result fileUploadExceptionHandler(MultipartException e) {
log.warn("上传文件异常:{}", e.getMessage());
return Result.error(ResultEnum.ERROR.getCode(), "文件过大,单个不超200M");
}
}

第二个问题对list进行操作(在入参的时候使用ValidList<xx>):

@Data
public class ValidList<E> implements List<E> {

@Valid
private List<E> list = new LinkedList<>();

@Override
public int size() {
return list.size();
}

@Override
public boolean isEmpty() {
return list.isEmpty();
}

@Override
public boolean contains(Object o) {
return list.contains(o);
}

@Override
public Iterator<E> iterator() {
return list.iterator();
}

@Override
public Object[] toArray() {
return list.toArray();
}

@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}

@Override
public boolean add(E e) {
return list.add(e);
}

@Override
public boolean remove(Object o) {
return list.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}

@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}

@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}

@Override
public void clear() {
list.clear();
}

@Override
public E get(int index) {
return list.get(index);
}

@Override
public E set(int index, E element) {
return list.set(index, element);
}

@Override
public void add(int index, E element) {
list.add(index, element);
}

@Override
public E remove(int index) {
return list.remove(index);
}

@Override
public int indexOf(Object o) {
return list.indexOf(o);
}

@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}

@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}

@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}

@Override
public List<E> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
}

----------------------------------------------------------------------------------------

1.概述

本文我们将重点介绍Spring中 @Valid@Validated注解的区别 。

验证用户输入是否正确是我们应用程序中的常见功能。Spring提供了@Valid和@Validated两个注解来实现验证功能,下面我们来详细介绍它们。

2. @Valid和@Validate注解

在Spring中,我们使用@Valid 注解进行方法级别验证,同时还能用它来标记成员属性以进行验证。

但是,此注释不支持分组验证。@Validated则支持分组验证。

3.例子

让我们考虑一个使用Spring Boot开发的简单用户注册表单。首先,我们只有名称密码属性:

public class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    // standard constructors / setters / getters / toString

}

接下来,让我们看一下控制器。在这里,我们将使用带有@Valid批注的saveBasicInfo方法来验证用户输入:

@RequestMapping(value = "/saveBasicInfo", method = RequestMethod.POST)
public String saveBasicInfo(
  @Valid @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, 
  ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

现在让我们测试一下这个方法:

@Test
public void givenSaveBasicInfo_whenCorrectInput`thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfo")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

在确认测试成功运行之后,现在让我们扩展功能。下一步的逻辑步骤是将其转换为多步骤注册表格,就像大多数向导一样。第一步,名称密码保持不变。在第二步中,我们将获取其他信息,例如agephone。因此,我们将使用以下其他字段更新域对象:

public class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    @Min(value = 18, message = "Age should not be less than 18")
    private int age;

    @NotBlank
    private String phone;

    // standard constructors / setters / getters / toString   

}

但是,这一次,我们将注意到先前的测试失败。这是因为我们没有传递年龄电话字段。

为了支持此行为,我们引入支持分组验证的@Validated批注。

分组验证,就是将字段分组,分别验证,比如我们将用户信息分为两组:BasicInfoAdvanceInfo

可以建立两个空接口:

public interface BasicInfo {
}
public interface AdvanceInfo {
}

第一步将具有BasicInfo接口,第二步 将具有AdvanceInfo 。此外,我们将更新UserAccount类以使用这些标记接口,如下所示:

public class UserAccount {

    @NotNull(groups = BasicInfo.class)
    @Size(min = 4, max = 15, groups = BasicInfo.class)
    private String password;

    @NotBlank(groups = BasicInfo.class)
    private String name;

    @Min(value = 18, message = "Age should not be less than 18", groups = AdvanceInfo.class)
    private int age;

    @NotBlank(groups = AdvanceInfo.class)
    private String phone;

    // standard constructors / setters / getters / toString   

}

另外,我们现在将更新控制器以使用@Validated注释而不是@Valid

@RequestMapping(value = "/saveBasicInfoStep1", method = RequestMethod.POST)
public String saveBasicInfoStep1(
  @Validated(BasicInfo.class) 
  @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

更新后,再次执行测试,现在可以成功运行。现在,我们还要测试这个新方法:

@Test
public void givenSaveBasicInfoStep1`whenCorrectInput`thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

也成功运行!

接下来,让我们看看@Valid对于触发嵌套属性验证是必不可少的。

4.使用@Valid批注标记嵌套对象

@Valid 可以用于嵌套对象。例如,在我们当前的场景中,让我们创建一个 UserAddress 对象:

public class UserAddress {

    @NotBlank
    private String countryCode;

    // standard constructors / setters / getters / toString
}

为了确保验证此嵌套对象,我们将使用@Valid批注装饰属性:

public class UserAccount {

    //...

    @Valid
    @NotNull(groups = AdvanceInfo.class)
    private UserAddress useraddress;

    // standard constructors / setters / getters / toString 
}

5. 总结

@Valid保证了整个对象的验证, 但是它是对整个对象进行验证,当仅需要部分验证的时候就会出现问题。 这时候,可以使用@Validated 进行分组验证。

----------------------------------------------------------------------------------------

 

----------------------------------------------------------------------------------------

标签:return,spring,boot,校验,value,public,Valid,private,class
From: https://www.cnblogs.com/hanease/p/16951568.html

相关文章

  • spring学习小结之:hibernatetemplate,过度封装?
    边学spring,突然发现之前spring与hibernate结合的方式可以更厉害地封装,那就是用hibernateTemplate了,只需要改边userdao.java如下importorg.springfr......
  • spring mvc3及mvc 3.2中的异常处理小结
    在springmvc3中,处理异常的是试用exceptionresolver去做异常,先来个简单DEMO看下:1)自定义异常类publicclassSpringExceptionextendsRuntime......
  • Spring中配合hibernate使用的简单例子
    最近在看SPRING和hibernate,学习了spring和hibernate之间整合,下面举出一个例子予以说明.    首先,可以注入的是sessionfactory,因为sessionfactory的Datasource可以......
  • spring 3 jdbc常用小结
    spring3的jdbc跟springmvc搭配起来挺好用的,本文试着小结其主要使用方法,并举出spring3.3.2中的一些信变化进行解析1)在dao中注入jdbctemplate,然后......
  • spring mvc中的拦截器小结
    在springmvc中,拦截器其实比较简单了,下面简单小结并demo下。preHandle:预处理回调方法,实现处理器的预处理(如登录检查),第三个参数为响应的处理器(如我们......
  • springboot_02
    1.修改配置 ky格式修改配置文件  1.1application.yml格式(一般用这种格式) ......
  • Firebase token认证 “kid“ invalid, unable to lookup correct key
    解码时,useFirebase\JWT\JWT;useFirebase\JWT\Key;JWT::decode($jwt,$key,['HS256']);改为JWT::decode($jwt,newKey($key,'HS256'));https://blog.csdn.net/we......
  • Spring实例化bean的方法
    Spring实例化bean的方法主要有四种:通过构造方法实例化bean,使用静态工厂实例化bean,使用实例工厂实例化bean,使用FactoryBean实例化bean。前三种了解即可,使用FactoryBean实例......
  • spring 动态获取配置spring获取所有配置spring运行中获取配置列表
    1:在任何springbean中注入PropertySourcesPlaceholderConfigurer对象,如:@AutowiredPropertySourcesPlaceholderConfigureraa; 2:获取当前配置中的值ObjectgetProp......
  • spring源码 自定义beanDefinition的添加逻辑
      首先我自定义了两个beandefinition,第一个会产生第二个,并且都是特殊的实现,能够避开前面的循环读取,所以,spring在最后用了一个while循环,一个flag就将,beandefinition新增......