点击上方“码农沉思录”链接
发现更多代码技巧
每次我们在搭建一个开源项目的首要任务包括:项目的统一异常处理、统一结果封装以及做项目的数据校验,在前后端分离的情况下,不仅前端需要做数据校验,同样后端也要实现,前端主要使用一些类似与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为快速失败返回模式:
@Configurationpublic 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层
@RestControllerpublic 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实体:
@Datapublic 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@AllArgsConstructorpublic 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@NoArgsConstructorpublic 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中有objectVOList的vipLevel是不是一个1<=x<=5的值,入参验证不会检测出来,这就是所谓的嵌套校验。
为了能够进行嵌套校验,必须手动在CreateRoomInfoVO实体的objectVOList字段上明确指出这个字段里的实体也要进行校验,由于@Validated不能在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证,也就是需要在嵌套校验的实体上加上注解@Valid,修改代码如下:
修改CreateRoomInfo类如下:
@JsonInclude(JsonInclude.Include.NON_NULL)@Data@NoArgsConstructor@AllArgsConstructorpublic 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实现实体的嵌套校验,数据校验在一个项目中扮演者不可轻视的角色,我们应该掌握如何高效的做好我们的后端数据校验。
点个在看吧,证明你还爱我