简介

说明

        本文介绍SpringBoot中参数验证的用法。

JSR-349,hibernate validator,Spring validation之间的关系

        JSR-349是一项标准,是JSR303的升级版。JSR-349添加了一些新特性,包括一些校验规范(校验注解),如@Null,@NotNull,@Pattern,它们位于javax.validation.constraints包下,只提供接口不提供实现。

        hibernate validator对这个规范的实现(不要将hibernate和数据库orm框架联系在一起),它们位于org.hibernate.validator.constraints包下。

        Spring为了给开发者提供便捷,对hibernate validator进行了二次封装。

@Valid与@Validated


不同点



@Valid



@Validated



来源



javax.validation的校验注解



Spring validation 的校验注解。



注解位置



方法参数、字段、构造方法、方法。



类、方法参数、方法。



分组



无分组功能。



有分组功能。

可在验证时根据不同的分组采用不同的验证机制。



嵌套



支持。

放在字段上面。



不支持


字段注解大全

字段注解(所属包:javax.validation.constraints.*)。


字段注解



说明



@AssertFalse



限制必须为false



@AssertTrue



限制必须为true



@DecimalMax(value)



限制必须为一个不大于指定值的数字



@DecimalMin(value)



限制必须为一个不小于指定值的数字



@Digits(integer,fraction)



限制必须为一个小数,且整数部分的位数不能超过integer,

小数部分的位数不能超过fraction



@Email



验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式



@Future



限制必须是一个将来的日期



@Max(value)



限制必须为一个不大于指定值的数字



@Min(value)



限制必须为一个不小于指定值的数字



@Past



限制必须是一个过去的日期



@Pattern(value)



限制必须符合指定的正则表达式



@NotEmpty



验证注解的元素值不为null或者空(isEmpty()方法)。

可用于字符串, Collection, Map, 数组



@NotBlank



验证注解的元素值不为null且包含除了空格之外至少一个字符。

只用于字符串



@Null



限制只能为null



@NotNull



限制必须不为null



@Past



验证注解的元素值(日期类型)比当前时间早



@Size(max,min)



限制字符长度必须在min到max之间


依赖

只需引入Spring的Validation即可。

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

它里边依赖了hibernate validator,就是下边这个

<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo_SpringBoot_MVC_validation</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

@Valid实例

代码

Controller

package com.example.demo.simple.controller;

import com.example.demo.simple.entity.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.List;

@Api(tags = "不分组")
@RestController
@RequestMapping("valid")
public class ValidController {

@ApiOperation("正常用法")
@GetMapping("normal")
public User normal(@Valid User user) {
return user;
}

@ApiOperation("获得BindingResult")
@GetMapping("bindingResult")
public User bindingResult(@Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()){
List<ObjectError> list = bindingResult.getAllErrors();
for (ObjectError objectError : list) {
System.out.println(objectError.getDefaultMessage());
}
//System.out.println(bindingResult.getFieldError().getDefaultMessage());
}

return user;
}

}

Entity

User类

package com.example.demo.simple.entity;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

@Data
public class User {
@NotBlank(message = "名字不能为空")
private String name;

private Integer age;

@NotBlank(message = "密码不能为空")
private String password;

@NotEmpty(message = "分数不能为空")
private List<Integer> scoreArray;

@Valid
@NotNull(message = "账户不能为null")
private Account account;
}

 Account类

package com.example.demo.simple.entity;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;

@Data
public class Account {
@NotBlank(message = "电话号码不能为空")
private String phoneNumber;

private String[] emails;
}

测试

        本文为了展示后端错误,直接用postman来进行请求。

        实际上,如果用了knife4j这个接口工具,它能自动识别是否为空的注解,并在前端进行控制。如果不能为空,则前端直接显示为红色的框。

SpringBoot--参数校验--@Valid/@Validated--使用/实例_网络协议

测试1:缺少字段

postman访问:​​http://localhost:8080/valid/normal​

postman访问结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_spring_02

后端结果:

2021-12-22 15:32:20.032  WARN 94176 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'user' on field 'account': rejected value [null]; codes [NotNull.user.account,NotNull.account,NotNull.com.example.demo.simple.entity.Account,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.account,account]; arguments []; default message [account]]; default message [账户不能为null]
Field error in object 'user' on field 'password': rejected value [null]; codes [NotBlank.user.password,NotBlank.password,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.password,password]; arguments []; default message [password]]; default message [密码不能为空]
Field error in object 'user' on field 'scoreArray': rejected value [null]; codes [NotEmpty.user.scoreArray,NotEmpty.scoreArray,NotEmpty.java.util.List,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.scoreArray,scoreArray]; arguments []; default message [scoreArray]]; default message [分数不能为空]]

测试2:不缺少字段

postman访问:​​http://localhost:8080/valid/normal​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_java_03

测试3:缺少字段,后端获取BindResult 

postman访问:​​http://localhost:8080/valid/bindingResult​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_网络协议_04

后端结果

账户不能为null
密码不能为空
分数不能为空

可以看到,没有报错。 

@Validated实例

不分组

跟上边“@Valid实例”的结果是一样的,只是把入参处的@Valid改为@Validated。

代码

Controller

package com.example.demo.validated.without_group.controller;

import com.example.demo.validated.without_group.entity.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.List;

@Api(tags = "不分组")
@RestController
@RequestMapping("validatedWithoutGroup")
public class ValidatedWithoutGroupController {

@ApiOperation("正常用法")
@GetMapping("normal")
public User normal(@Validated User user) {
return user;
}

@ApiOperation("获得BindingResult")
@GetMapping("bindingResult")
public User bindingResult(@Validated User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()){
List<ObjectError> list = bindingResult.getAllErrors();
for (ObjectError objectError : list) {
System.out.println(objectError.getDefaultMessage());
}
//System.out.println(bindingResult.getFieldError().getDefaultMessage());
}

return user;
}

}

Entity

User类

package com.example.demo.validated.without_group.entity;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

@Data
public class User {
@NotBlank(message = "名字不能为空")
private String name;

private Integer age;

@NotBlank(message = "密码不能为空")
private String password;

@NotEmpty(message = "分数不能为空")
private List<Integer> scoreArray;

@Valid
@NotNull(message = "账户不能为null")
private Account account;
}

Account类

package com.example.demo.validated.without_group.entity;

import lombok.Data;

import javax.validation.constraints.NotBlank;

@Data
public class Account {
@NotBlank(message = "电话号码不能为空")
private String phoneNumber;

private String[] emails;
}

测试

测试1:缺少字段

postman访问:​​http://localhost:8080/validatedWithoutGroup/normal​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_字段_05

后端结果:

2021-12-22 16:12:26.549  WARN 79176 --- [nio-8080-exec-7] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'user' on field 'scoreArray': rejected value [null]; codes [NotEmpty.user.scoreArray,NotEmpty.scoreArray,NotEmpty.java.util.List,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.scoreArray,scoreArray]; arguments []; default message [scoreArray]]; default message [分数不能为空]
Field error in object 'user' on field 'password': rejected value [null]; codes [NotBlank.user.password,NotBlank.password,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.password,password]; arguments []; default message [password]]; default message [密码不能为空]
Field error in object 'user' on field 'account': rejected value [null]; codes [NotNull.user.account,NotNull.account,NotNull.com.example.demo.validated.without_group.entity.Account,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.account,account]; arguments []; default message [account]]; default message [账户不能为null]]

测试2:不缺少字段

postman访问:​​http://localhost:8080/validatedWithoutGroup/normal​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_spring_06

测试3: 获得BindingResult

postman访问:​​http://localhost:8080/validatedWithoutGroup/bindingResult​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_字段_07

后端结果:

分数不能为空
密码不能为空
账户不能为null

分组

代码

Controller

package com.example.demo.validated.with_group.controller;

import com.example.demo.validated.with_group.entity.User;
import com.example.demo.validated.with_group.validatation.IGroupA;
import com.example.demo.validated.with_group.validatation.IGroupAll;
import com.example.demo.validated.with_group.validatation.IGroupB;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "分组")
@RestController
@RequestMapping("validatedWithGroup")
public class ValidatedWithGroupController {
@ApiOperation("使用组:GroupA")
@GetMapping("groupA")
public User groupA(@Validated({IGroupA.class}) User user) {
return user;
}

@ApiOperation("使用组:GroupB")
@GetMapping("groupB")
public User groupB(@Validated({IGroupB.class}) User user) {
return user;
}

@ApiOperation("使用组:GroupA和GroupB")
@GetMapping("groupAAndGroupB")
public User groupAAndGroupB(@Validated({IGroupA.class, IGroupB.class}) User user) {
return user;
}

@ApiOperation("使用组:GroupAll")
@GetMapping("groupAll")
public User groupAll(@Validated({IGroupAll.class}) User user) {
return user;
}

}

Entity

User类

package com.example.demo.validated.with_group.entity;

import com.example.demo.validated.with_group.validatation.IGroupA;
import com.example.demo.validated.with_group.validatation.IGroupB;
import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

@Data
public class User {
@NotBlank(message = "名字不能为空")
private String name;

@NotNull(message = "年龄不能为空", groups = {IGroupA.class})
private Integer age;

@NotEmpty(message = "密码不能为空", groups = {IGroupB.class})
private String password;

@NotEmpty(message = "分数不能为空", groups = {IGroupA.class, IGroupB.class})
private List<Integer> scoreArray;

@Valid
@NotNull(message = "账户不能为null")
private Account account;
}

Account类

package com.example.demo.validated.with_group.entity;

import lombok.Data;

import javax.validation.constraints.NotEmpty;

@Data
public class Account {
@NotEmpty(message = "电话号码不能为空")
private String phoneNumber;

private String[] emails;
}

Group

IGroupA接口

package com.example.demo.validated.with_group.validatation;

public interface IGroupA {
}

IGroupB接口 

package com.example.demo.validated.with_group.validatation;

public interface IGroupB {
}

 IGroupAll接口 

package com.example.demo.validated.with_group.validatation;

import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({Default.class, IGroupA.class, IGroupB.class})
public interface IGroupAll {
}

测试

测试1:无参数请求groupA

postman访问:​​http://localhost:8080/validatedWithGroup/groupA​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_字段_08

后端结果:

2021-12-22 16:32:44.138  WARN 85532 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'user' on field 'scoreArray': rejected value [null]; codes [NotEmpty.user.scoreArray,NotEmpty.scoreArray,NotEmpty.java.util.List,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.scoreArray,scoreArray]; arguments []; default message [scoreArray]]; default message [分数不能为空]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]]

测试2:无参数请求groupB

postman访问:​​http://localhost:8080/validatedWithGroup/groupB​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_字段_09

后端结果:

2021-12-22 16:33:15.773  WARN 85532 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'user' on field 'scoreArray': rejected value [null]; codes [NotEmpty.user.scoreArray,NotEmpty.scoreArray,NotEmpty.java.util.List,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.scoreArray,scoreArray]; arguments []; default message [scoreArray]]; default message [分数不能为空]
Field error in object 'user' on field 'password': rejected value [null]; codes [NotEmpty.user.password,NotEmpty.password,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.password,password]; arguments []; default message [password]]; default message [密码不能为空]]

测试3:无参数请求groupA和groupB

postman访问:​​http://localhost:8080/validatedWithGroup/groupAAndGroupB​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_网络协议_10

后端结果:

2021-12-22 16:34:27.652  WARN 85532 --- [nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'user' on field 'scoreArray': rejected value [null]; codes [NotEmpty.user.scoreArray,NotEmpty.scoreArray,NotEmpty.java.util.List,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.scoreArray,scoreArray]; arguments []; default message [scoreArray]]; default message [分数不能为空]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
Field error in object 'user' on field 'password': rejected value [null]; codes [NotEmpty.user.password,NotEmpty.password,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.password,password]; arguments []; default message [password]]; default message [密码不能为空]]

测试4:无参数请求groupAll

postman访问:​​http://localhost:8080/validatedWithGroup/groupAll​

postman结果:

SpringBoot--参数校验--@Valid/@Validated--使用/实例_spring_11

后端结果:

2021-12-22 16:36:54.095  WARN 91820 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'user' on field 'name': rejected value [null]; codes [NotBlank.user.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [名字不能为空]
Field error in object 'user' on field 'account': rejected value [null]; codes [NotNull.user.account,NotNull.account,NotNull.com.example.demo.validated.with_group.entity.Account,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.account,account]; arguments []; default message [account]]; default message [账户不能为null]]

可以看到:走的校验逻辑是没有除了IGroupA和IGroupB注解的字段的逻辑。

自定义校验注解

也可以自定义校验的注解,自己写校验逻辑。

后续有时间补充。