每次我们在搭建一个开源项目的首要任务包括:项目的统一异常处理、统一结果封装以及做项目的数据校验,在前后端分离的情况下,不仅前端需要做数据校验,同样后端也要实现,前端主要使用一些类似与jQuery Validate等js/css插件实现通过数据校验,比如:bootstrap-validator,而后端主要使用的是Hibernate Validator检验框架,通过数据校验,我们能避免用户借助一些HTTP请求工具直接向后端发送一些不合法的数据请求,本文将入如何在Spring/Spring Boot下实现后端的数据校验。
官网地址:http://hibernate.org/validator/
注解 |
用途 |
---|---|
Valid |
递归的对关联的对象进行校验 |
AssertFalse |
用于boolean字段,该字段的值只能为false |
AssertTrue |
用于boolean字段,该字段只能为true |
DecimalMax(value) |
被注释的元素必须是一个数字,只能大于或等于该值 |
DecimalMin(value) |
被注释的元素必须是一个数字,只能小于或等于该值 |
Digits(integer,fraction) |
检查是否是一种数字的(整数,小数)的位数 |
Future |
检查该字段的日期是否是属于将来的日期 |
FutureOrPresent |
判断日期是否是将来或现在日期 |
Past |
检查该字段的日期是在过去 |
PastOrPresent |
判断日期是否是过去或现在日期 |
Max(value) |
该字段的值只能小于或等于该值 |
Min(value) |
该字段的值只能大于或等于该值 |
Negative |
判断负数 |
NegativeOrZero |
判断负数或0 |
Positive |
判断正数 |
PositiveOrZero |
判断正数或0 |
NotNull |
不能为null |
Null |
必须为 null |
Pattern(value) |
被注释的元素必须符合指定的正则表达式 |
Size(max, min) |
检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等 |
Length(max, min) |
判断字符串长度 |
CreditCardNumber |
被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
|
被注释的元素必须是电子邮箱地址 |
Length(min=, max=) |
被注释的字符串的大小必须在指定的范围内 |
NotBlank |
只能用于字符串不为null,并且字符串trim()以后length要大于0 |
NotEmpty |
集合对象的元素不为0,即集合不为空,也可以用于字符串不为null |
Range(min=, max=) |
被注释的元素必须在合适的范围内 |
SafeHtml |
classpath中要有jsoup包 |
ScriptAssert |
要有Java Scripting API 即JSR 223("Scripting for the JavaTMPlatform")的实现 |
URL(protocol=,host=,port=,regexp=,flags=) |
被注释的字符串必须是一个有效的url |
org.hibernate.validator
hibernate-validator
6.1.0.Final
注意:如果你是SpringBoot项目,上述依赖不需要导入,因为spring-boot-starter-web
包里面有hibernate-validator
包,不需要引用hibernate validator依赖。
Hibernate Validator有以下两种验证模式:
普通模式(默认就是这个模式)
普通模式(会校验完所有的属性,然后返回所有的验证失败信息)
2. 快速失败返回模式
快速失败返回模式(只要有一个验证失败,则返回)
两种验证模式配置方式:参考 hibernate 官方文档
failFast:true 快速失败返回模式 false 普通模式
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.addMapping( (ConstraintMapping) null )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
和 (hibernate.validator.fail_fast:true 快速失败返回模式 false 普通模式)
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
我们可以通过配置文件设置hibernate Validator为快速失败返回模式:
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
@Valid 和 BindingResult 是一一对应的,如果有多个@Valid,那么每个@Valid后面跟着的BindingResult就是这个@Valid的验证结果,顺序不能乱,如果有多个@Valid,那么需要多个BindingResult来保存校验结果,首先我们需要在我们的实体上定义校验规则:
/**
* @Author 林必昭
* @Date 2019/11/23 14:08
*/
public class ValidationDemo {
private Integer id;
@Length(min = 4, max = 8, message = "用户名长度要求在{min}-{max}之间")
@NotNull(message = "用户名不可为空")
private String username;
@Email(message = "邮箱格式错误")
private String email;
@Past(message = "出生日期错误")
private Date birthday;
@Min(value = 18, message = "未成年不满足注册要求")
@Max(value = 80, message = "年龄错误")
private Integer age;
@Range(min = 0, max = 1, message = "性别选择错误")
private Integer sex;
}
Controller层
@RestController
public class ValidationController {
/**
* @Valid 表示对该实体进行校验
* @param demo 待校验实体
* @param bindingResult 保存对参数的校验结果
* @return
*/
@RequestMapping(value = "validation", method = RequestMethod.POST)
public JSONResult validate(@Valid @RequestBody ValidationDemo demo, BindingResult bindingResult){
JSONResult jsonResult = new JSONResult();
if (bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(e ->{
jsonResult.setState(ApiResponseCodeEnum.ERROR.getCode());
jsonResult.setData("校验失败");
jsonResult.setMsg(e.getDefaultMessage());
});
}else {
jsonResult.setState(ApiResponseCodeEnum.SUCCESS.getCode());
jsonResult.setData("校验成功");
jsonResult.setMsg("");
}
return jsonResult;
}
}
RequestBody
{
"username": "jacklinjacklin",
"email": "[email protected]",
"birthday": "2019-04-14T09:05:39.604Z",
"age": 22,
"sex": 0
}
Response
{
"state": -1,
"data": "校验失败",
"msg": "用户名长度要求在4-8之间"
}
RequestBody
{
"username": "jacklin",
"email": "string",
"birthday": "2019-04-14T09:05:39.604Z",
"age": 22,
"sex": 0
}
Response
{
"state": -1,
"data": "校验失败",
"msg": "邮箱格式错误"
}
RequestBody
{
"username": "jacklin",
"email": "[email protected]",
"birthday": "2019-04-14T09:05:39.604Z",
"age": 22,
"sex": 0
}
Response
{
"state": 200,
"data": "校验成功",
"msg": ""
}
由此可见,参数校验已经生效,如果email不符合格式或者用户名长度等不符合Spring都会帮我们校验出错误,具体的@Email是如何检验的,可以查看@Email的实现EmailValidator.java,这里需要注意的是待校验实体必须生成getter和setter方法,否则我们在控制层上接收到的入参发现都是null值,我们可以使用lombok@Data注解快速生成。
我们在编写控制层提供服务api时,有些时候从前端传过来的参数较多,比较好的办法是定义一个实体类来封装请求参数,但是用实体类封装参数后,无法对参数值进行校验,可以使用spring的@Validated 结合java validation、hibernate validation注解进行校验。Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
1. 分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
2. 注解地方
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
3. 嵌套验证
表示一个校验实体中还嵌套者另一个待校验实体,需要同时对他们进行校验
添加校验注解的方式固然是方便的,但是如果一个实体对象在不同的业务中的校验规则不同的话,难道我们需要编写两个Object对象么?答案肯定不是,那么这时候就用到分组校验,对不同的校验规则进行隔离校验,互相不受影响。
比如,创建UserEntity实体:
@Data
public class UserEntity implements Serializable {
//定义分组
public interface AddGroup{}
public interface UpdateGroup{}
private static final long serialVersionUID = -7375937748809705055L;
/**用户ID*/
private Integer userId;
/**用户名*/
@NotBlank(message="用户名不能为空", groups = {AddGroup.class})
private String username;
/**
* 邮箱
*/
@Email(message="邮箱格式不正确", groups = {AddGroup.class, UpdateGroup.class})
private String email;
@Min(value = 18,message = "未成年不能注册")
private Integer age;
/**
* 手机号
*/
@NotNull(message = "手机号不能为空")
private String mobile;
}
Controller
@RequestMapping(value = "create", method = RequestMethod.POST)
public JSONResult save(@Validated({AddGroup.class}) @RequestBody UserEntity userEntity, BindingResult bindingResult){
//doSomething
}
@RequestMapping(value = "update", method = RequestMethod.POST)
public JSONResult update(@Validated({UpdateGroup.class}) @RequestBody UserEntity userEntity, BindingResult bindingResult){
//doSomething
}
通过分析上面的代码,我们来理解Hibernate Validator校验框架的使用,其中,username属性,表示只有新增/保存的时候,才会校验username属性,而email属性,无论是保存或者更新的得时候都会校验email属性,如果不指定groups,则默认使用javax.validation.groups.Default.class分组,可以通过ValidatorUtils.validateEntity(user)进行校验。
什么是嵌套验证?顾名思义,嵌套验证就是一个实体中的属性包含其他实体,在对当前实体做校验的同时,还要对其属性的实体进行嵌套验证,比如,我们现在有一个实体CreateRoomInfoVO,通过Java+Hibernate校验Api请求vo实体,在实体的属性上添加校验规则,在API接收数据时添加@Valid注解,这时你的实体将会开启一个校验的功能。
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateRoomInfoVO implements Serializable {
@NotNull(message = "用户名不能为空")
private String username;
@NotNull(message = "手机号码不能为空")
private String mobile;
@NotNull(message = "身份证号码不能为空")
private String identify;
private Boolean carProvide;
private Boolean driverProvide;
@Min(value = 18, message = "年龄错误")
@Max(value = 80, message = "年龄错误")
private Integer age;
@Range(min = 0, max = 1, message = "性别选择错误")
private Integer sex;
@NotNull(message = "objectVOList不能为空")
private List objectVOList;
}
可以看到,CreateRoomInfoVO带有很多属性,属性里包括username,mobile,identify,carProvide,driverProvide,还包括一嵌套实体属性objectVOList。
ObjectVO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ObjectVO {
private Integer id;
@Min(1)
@Max(value = 5,message = "vip等级最多为5")
private Integer vipLevel;
}
Controller
@RequestMapping(value = "nested/validation", method = RequestMethod.POST)
public JSONResult nestedValidate(@Valid @RequestBody CreateRoomInfoVO createRoomInfoVO, BindingResult bindingResult){
JSONResult jsonResult = new JSONResult();
if (bindingResult.hasErrors()) {
bindingResult.getAllErrors().forEach(e ->{
jsonResult.setState(ApiResponseCodeEnum.ERROR.getCode());
jsonResult.setData("校验失败");
jsonResult.setMsg(e.getDefaultMessage());
});
}else {
jsonResult.setState(ApiResponseCodeEnum.SUCCESS.getCode());
jsonResult.setData("校验成功");
jsonResult.setMsg("");
}
return jsonResult;
}
RequestBody
{
"username": "jacklin",
"mobile": "19120557972",
"identify": "440881199608286719",
"carProvide": true,
"driverProvide": false,
"age": 22,
"sex": 0,
"objectVOList":[{
"id": 1,
"vipLevel": 6
}]
}
Response
{
"state": 200,
"data": "校验成功",
"msg": ""
}
这里返回校验成功,说明我们的嵌套校验还没生效!!
在上图中,如果CreateRoomInfoVO实体的objectVOList属性不额外加注释,只有@NotNull,无论入参采用@Validated还是@Valid验证,从返回知道,我们传vipLevel的值为6,本质上应该嵌套校验是不能通过的,原因是Spring Validation框架只会对CreateRoomInfoVO的username,mobile,identify,carProvide,driverProvide和objectVOList的字段做非空校验,不会对CreateRoomInfoVO字段里的ObjectVO实体vipLevel字段做等级校验,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List
为了能够进行嵌套校验,必须手动在CreateRoomInfoVO实体的objectVOList字段上明确指出这个字段里的实体也要进行校验,由于@Validated不能在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证,也就是需要在嵌套校验的实体上加上注解@Valid,修改代码如下:
修改CreateRoomInfo类如下:
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateRoomInfoVO implements Serializable {
@NotNull(message = "用户名不能为空")
private String username;
@NotNull(message = "手机号码不能为空")
private String mobile;
@NotNull(message = "身份证号码不能为空")
private String identify;
private Boolean carProvide;
private Boolean driverProvide;
@Min(value = 18, message = "年龄错误")
@Max(value = 80, message = "年龄错误")
private Integer age;
@Range(min = 0, max = 1, message = "性别选择错误")
private Integer sex;
@Valid
@NotNull(message = "objectVOList不能为空")
private List objectVOList;
}
再次发送请求:
Response
{
"state": -1,
"data": "校验失败",
"msg": "vip等级最多为5"
}
结果发现通过在待校验的嵌套实体ObjcetVOList上注解@Vaild,我们的嵌套校验才生效,这时候就能对CreateRoomInfoVO的入参进行嵌套验证了,此时CreateRoomInfoVO里面的objectVOList如果含有ObjectVO的相应字段为空的情况,Spring Validation框架都会检测出来,bindingResult就会记录相应的错误。
@Validated:提供分组校验功能,可以在入参的时,根据不同的分组用不同的校验机制,用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid:没有分组校验功能,用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。需要在黛娇妍对象注解@Valid进行嵌套验证。
通过该篇文章,我们讲解了hibernate的常见校验注解的使用、hibernate的两种校验模式的区别和配置实现、hibernate的分组校验规则、详细说明了@Validated和@Valid注解的区别以及使用@Valid实现实体的嵌套校验,数据校验在一个项目中扮演者不可轻视的角色,我们应该掌握如何高效的做好我们的后端数据校验。