专栏首页Java架构沉思录如何实现Java后端数据校验?看这篇就足够!

如何实现Java后端数据校验?看这篇就足够!

每次我们在搭建一个开源项目的首要任务包括:项目的统一异常处理、统一结果封装以及做项目的数据校验,在前后端分离的情况下,不仅前端需要做数据校验,同样后端也要实现,前端主要使用一些类似与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计算合法性

Email

被注释的元素必须是电子邮箱地址

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

Maven依赖

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>

注意:如果你是SpringBoot项目,上述依赖不需要导入,因为spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。

heibernate的校验模式

Hibernate Validator有以下两种验证模式:

  1. 普通模式(默认就是这个模式)

普通模式(会校验完所有的属性,然后返回所有的验证失败信息)

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": "793066408@qq.com",
  "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": "793066408@qq.com",
  "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注解快速生成。

谈谈@Validated和@Valid的区别?

我们在编写控制层提供服务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<ObjectVO> 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<ObjectVO>中有objectVOList的vipLevel是不是一个1<=x<=5的值,入参验证不会检测出来,这就是所谓的嵌套校验。

为了能够进行嵌套校验,必须手动在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<ObjectVO> objectVOList;
}

再次发送请求:

Response

{
    "state": -1,
    "data": "校验失败",
    "msg": "vip等级最多为5"
}

结果发现通过在待校验的嵌套实体ObjcetVOList上注解@Vaild,我们的嵌套校验才生效,这时候就能对CreateRoomInfoVO的入参进行嵌套验证了,此时CreateRoomInfoVO里面的objectVOList如果含有ObjectVO的相应字段为空的情况,Spring Validation框架都会检测出来,bindingResult就会记录相应的错误。

@Validated和@Valid注解的使用区别

@Validated:提供分组校验功能,可以在入参的时,根据不同的分组用不同的校验机制,用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

@Valid:没有分组校验功能,用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。需要在黛娇妍对象注解@Valid进行嵌套验证。

总结

通过该篇文章,我们讲解了hibernate的常见校验注解的使用、hibernate的两种校验模式的区别和配置实现、hibernate的分组校验规则、详细说明了@Validated和@Valid注解的区别以及使用@Valid实现实体的嵌套校验,数据校验在一个项目中扮演者不可轻视的角色,我们应该掌握如何高效的做好我们的后端数据校验。

本文分享自微信公众号 - Java架构沉思录(code-thinker),作者:林必昭

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 浅析 Nginx 网络事件

    Nginx 是一个事件驱动的框架,所谓事件主要指的是网络事件,Nginx 每个网络连接会对应两个网络事件,一个读事件一个写事件。在深入了解 Nginx 各种原理...

    黄泽杰
  • ThreadLocal面试六连问,你能Hold住吗?

    中高级阶段开发者出去面试,应该躲不开ThreadLocal相关问题,本文就常见问题做出一些解答,欢迎留言探讨。

    黄泽杰
  • 想不到吧?我是这样用Redis实现消息定时推送的!

    先说一下领劵中心的项目吧,这个项目就类似京东app的领劵中心,当然图是截取京东的,公司的就不截了。。。

    黄泽杰
  • Spring Boot 参数校验详解

    在 Spring Boot 的官网中,关于Validation只是简单的提了一句,如下

    南风
  • 芋道 Spring Boot 参数校验 Validation 入门

    当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是必不可少的活。例如说,用户注册时,会校验手机格式的正确性,密码非弱密码。

    芋道源码
  • 规范-前、后台请求参数校验

    正常情况下,前后端对于请求的参数都需要校验的,这能提高应用程序的稳定性、可维护性,而对于前后台如果能将这种不可缺少校验规则汇总并制定一套规范,在每一个应用程序中...

    秋日芒草
  • Java数据校验详解

    一个健壮的系统都要对外部提交的数据进行完整性、合法性的校验。即使开发一个不面对最终用户的工具包,也需要对传入的数据进行缜密的校验来防止引发底层难以追踪的问题。各...

    随风溜达的向日葵
  • 使用Spring Boot进行参数校验

    原文:cnblogs.com/cjsblog/p/8946768.html 编辑自公众号:Java后端

    好好学java
  • 使用Spring Boot进行参数校验

    在Spring Boot的官网中,关于Validation只是简单的提了一句,如下

    用户1093975
  • Java数据校验详解

    一个健壮的系统都要对外部提交的数据进行完整性、合法性的校验。即使开发一个不面对最终用户的工具包,也需要对传入的数据进行缜密的校验来防止引发底层难以追踪的问题。各...

    随风溜达的向日葵

扫码关注云+社区

领取腾讯云代金券