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

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

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

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

二类是数据的校验需要和数据库交互的,比如是否唯一(数据库中是否存在)、数量限制(数据库中只能允许存在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 进行分组验证。

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

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

spring-boot-@Valid和@Validated详解的更多相关文章

  1. Spring Boot的启动器Starter详解

    Spring Boot的启动器Starter详解 作者:chszs,未经博主允许不得转载.经许可的转载需注明作者和博客主页:http://blog.csdn.net/chszs Spring Boot ...

  2. Spring Boot 之使用 Json 详解

    Spring Boot 之使用 Json 详解 简介 Spring Boot 支持的 Json 库 Spring Web 中的序列化.反序列化 指定类的 Json 序列化.反序列化 @JsonTest ...

  3. Spring Boot(八):RabbitMQ详解

    Spring Boot(八):RabbitMQ详解 RabbitMQ 即一个消息队列,主要是用来实现应用程序的异步和解耦,同时也能起到消息缓冲,消息分发的作用. 消息中间件在互联网公司的使用中越来越多 ...

  4. Spring boot注解(annotation)含义详解

    Spring boot注解(annotation)含义详解 @Service用于标注业务层组件@Controller用于标注控制层组件(如struts中的action)@Repository用于标注数 ...

  5. Spring Boot Actuator监控使用详解

    在企业级应用中,学习了如何进行SpringBoot应用的功能开发,以及如何写单元测试.集成测试等还是不够的.在实际的软件开发中还需要:应用程序的监控和管理.SpringBoot的Actuator模块实 ...

  6. Spring Boot中@ConditionalOnProperty使用详解

    在Spring Boot的自动配置中经常看到@ConditionalOnProperty注解的使用,本篇文章带大家来了解一下该注解的功能. Spring Boot中的使用 在Spring Boot的源 ...

  7. Spring Boot启动命令参数详解及源码分析

    使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...

  8. Spring Boot 之属性读写详解

    SpringBoot 之属性读写详解 加载 property 顺序 随机属性 命令行属性 Application 属性文件 Profile 特定属性 属性中的占位符 YAML 属性 访问属性 多 pr ...

  9. Spring Boot的SpringApplication类详解

    相信使用过Spring Boot的开发人员,都对Spring Boot的核心模块中提供的SpringApplication类不陌生.SpringApplication类的run()方法往往在Sprin ...

  10. spring boot application properties配置详解

    # =================================================================== # COMMON SPRING BOOT PROPERTIE ...

随机推荐

  1. dotnet core微服务框架Jimu ~ 会员授权微服务

    提供授权服务,用户使用会员的用户名和密码获取 token, 带着 token 访问受保护的接口,如浏览和发布新闻. 有 2 个公开的 api: token: 获取 token; GetCurrentM ...

  2. HJ17 坐标移动问题 ——秋招笔试

    HJ17 坐标移动问题 华为笔试[难度中等] 问题描述: 开发一个坐标计算工具, A表示向左移动,D表示向右移动,W表示向上移动,S表示向下移动.从(0,0)点开始移动,从输入字符串里面读取一些坐标, ...

  3. 支持国产3A游戏大作 ——《黑神话:悟空》

  4. 鸿蒙接入Flutter3.22

    配置环境变量 配置HarmonyOS SDK和环境变量 API12, deveco-studio-5.0 或 command-line-tools-5.0 配置 Java17 配置环境变量 (SDK, ...

  5. 查看Mysql数据库数据量大小、表大小、索引大小

    通过MySQL的information_schema数据库,可查询数据库中每个表占用的空间.表记录的行数: 该库中有一个TABLES表,这个表主要字段分别是: TABLE_SCHEMA:数据库名 TA ...

  6. Codeforces 1847 A-F

    题面 A B C D E F 难度:红 橙 黄 蓝 紫 紫 题解 B 题目大意:找到一组分割方法,使得 \(\sum _{i=1} ^ k (\text{&} _{j=l_i} ^ {r_i} ...

  7. oracle查询是否锁表以及解锁语句

    --锁表语句 SELECT b.owner, b.object_name, a.session_id, a.locked_mode FROM v$locked_object a, dba_object ...

  8. Web实时消息推送的解决方案

    什么是消息推送(push) 推送的场景比较多,比如有人关注我的公众号,这时我就会收到一条推送消息,以此来吸引我点击打开应用. 消息推送(push)通常是指网站的运营工作等人员,通过某种工具对用户当前网 ...

  9. golang之类型转换cast

    Go 语言作为强类型语言,在使用 Golang 开发项目时,经常会遇到类型转换的场景,整型之间可以直接转换,字节切片和字符串之间也可以直接转换. 但是,如果整型和字符串之间做类型转换,则需要使用 st ...

  10. 调度系统之Airflow

    一.Airflow简介 Airflow 是一个使用 Python 语言编写的 Data Pipeline 调度和监控工作流的平台. Airflow 是通过 DAG(Directed acyclic g ...