【BeanValidation】一、springboot项目以注解方式实现后端数据验证(javax.validation.api与hibernate-validator)

  • Post author:
  • Post category:java




1 前言



1.1 场景

最近的这个项目是互联网项目,网络用户需要填写表单信息保存提交,前端提交信息需要在后端进行数据格式校验,从而避免无效数据提交到数据库。常见的数据检查工作包括必填项检查、数值检查、长度检查、身份证号码、手机号码检查等。

如果将这些字段校验和业务逻辑混合一起写,用大量if else代码判断,则会干扰原有逻辑,而且不容易维护。针对该问题,实际上,Java 早在 2009 年就提出了 Bean Validation 规范,该规范定义的是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。下面即将要介绍的是该数据验证的规范,以及相应的技术框架。



1.2 名词或技术介绍



(1) JSR规范提案

JSR:Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务,JSR已成为Java界的一个重要标准。

本文介绍的Bean Validation 就是出自JSR303,JSR349,以及JSR380 规范提案。该规范从JSR 303 发展到 JSR 380,目前最新规范是Bean Validation 2.0。

相信有小伙伴想去看下到底是个啥。

规范提案地址:https://jcp.org/en/jsr/summary?id=bean+validation


在这里插入图片描述

需要注意的是,规范提案只是提供了规范,并没有提供具体的实现。具体实现框架有默认的javax.validation.api,以及hibernate-validator。目前绝大多使用hibernate-validator,下面简要介绍两个框架。



(2) javax.validation.api

Java 在2009年的 JAVAEE 6 中发布了 JSR303以及javax下的validation包内容。

这项工作的主要目标是为java应用程序开发人员提供 基于java对象的 约束(constraints)声明 和 对约束的验证工具(validator),以及约束元数据存储库和查询API,以及默认实现。

Java8开始,Java EE改名为Jakarta EE,注意javax.validation相关的包移动到了jakarta.validation的包下。所以大家看不同的版本的时候,会发现以前的版本包在javax.validation包下,java 8之后在jakarta.validation。

JakartaEE的官网及其支持的项目:https://jakarta.ee/



Bean Validation 2.0规范及默认实现的地址:https://beanvalidation.org/2.0/spec/#whatsnew

在这里插入图片描述



(3) hibernate-validator

Hibernate-Validator框架是另外一个针对Bean Validation 规范的实现,它提供了 JSR 380 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。(注意:此处的Hibernate 不是 Hibernate ORM没有任何关系,hibernate-validator是Hibernate 基金会下的项目之一)

hibernate-validator的介绍地址:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/


hibernate-validator源码地址:https://github.com/hibernate/hibernate-validator



2 Bean Validation 定义的约束注解

下面列出常用约束,每个约束都有参数 message,groups 和 payload。这是 Bean Validation 规范的要求。

请注意,这里没有区分哪些是默认注解,哪些是hibernate-validator的注解,因为我们实际开发中,会同时用两个包。



(1)2个标识注解

@Valid(规范、常用)

标记用于验证级联的属性、方法参数或方法返回类型。

在验证属性、方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。

@Validated(spring扩展提供)

spring 提供的扩展注解,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验,可以方便的用于分组校验。其中,message 是提示消息,groups 可以根据情况来分组。当然,也可以不使用注解,而直接使用Validator.validate()来进行分组验证。



(2)22个约束注解

以下每一个注解都可以在相同元素上定义多个。

@AssertFalse

检查元素是否为 false,支持数据类型:boolean

@AssertTrue

检查元素是否为 true,支持数据类型:boolean

@DecimalMax(value=, inclusive=)

inclusive:boolean,默认 true,表示是否包含,是否等于

value:当 inclusive=false 时,检查带注解的值是否小于指定的最大值。当 inclusive=true 检查该值是否小于或等于指定的最大值。参数值是根据 bigdecimal 字符串表示的最大值。

支持数据类型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封装类)

@DecimalMin(value=, inclusive=)

支持数据类型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封装类)

inclusive:boolean,默认 true,表示是否包含,是否等于

value:

当 inclusive=false 时,检查带注解的值是否大于指定的最大值。当 inclusive=true 检查该值是否大于或等于指定的最大值。参数值是根据 bigdecimal 字符串表示的最小值。

@Digits(integer=, fraction=)

检查值是否为最多包含 integer 位整数和 fraction 位小数的数字

支持的数据类型:

BigDecimal, BigInteger, CharSequence, byte, short, int, long 、原生类型的封装类、任何 Number 子类。

@Email

检查指定的字符序列是否为有效的电子邮件地址。可选参数 regexp 和 flags 允许指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)。

支持的数据类型:CharSequence

@Max(value=)

检查值是否小于或等于指定的最大值

支持的数据类型:

BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类

@Min(value=)

检查值是否大于或等于指定的最大值

支持的数据类型:

BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类

@NotBlank

检查字符序列是否为空,以及去空格后的长度是否大于 0。与 @NotEmpty 的不同之处在于,此约束只能应用于字符序列。

支持数据类型:CharSequence

@NotNull

检查值是否不为 null

支持数据类型:任何类型

@NotEmpty

检查元素是否为 null 或 空

支持数据类型:CharSequence, Collection, Map, arrays

@Size(min=, max=)

检查元素个数是否在 min(含)和 max(含)之间

支持数据类型:CharSequence,Collection,Map, arrays

@Negative

检查元素是否严格为负数。零值被认为无效。

支持数据类型:

BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类

@NegativeOrZero

检查元素是否为负或零。

支持数据类型:

BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类

@Positive

检查元素是否严格为正。零值被视为无效。

支持数据类型:

BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类

@PositiveOrZero

检查元素是否为正或零。

支持数据类型:

BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类

@Null

检查值是否为 null

支持数据类型:任何类型

@Future

检查日期是否在未来

支持的数据类型:

java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate

如果 Joda Time API 在类路径中,ReadablePartial 和ReadableInstant 的任何实现类

@FutureOrPresent

检查日期是现在或将来

支持数据类型:同@Future

@Past

检查日期是否在过去

支持数据类型:同@Future

@PastOrPresent

检查日期是否在过去或现在

支持数据类型:同@Future

@Pattern(regex=, flags=)

根据给定的 flag 匹配,检查字符串是否与正则表达式 regex 匹配

支持数据类型:CharSequence



(3) Hibernate Validator 附加的约束注解

org.hibernate.validator.constraints 包下,定义了一系列的约束( constraint )注解。如下:

@Range(min=, max=) :被注释的元素必须在合适的范围内。

@Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。

@URL(protocol=,host=,port=,regexp=,flags=) :被注释的字符串必须是一个有效的 URL 。

@SafeHtml :判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。

… 等等,就不一一列举了。



3 代码实战

本节代码将采用springboot框架,结合springAOP和AspectJ,以声明式注解的方式实现:

(1) 对需要验证的字段,使用hibernate-validator提供的注解进行标注;

(2) 对项目中需要进行数据验证的controller接口,采用自定义注解的方式标注,并且以自定义格式将不符合要求的说明返回。



3.1 Jar包引入

需要引入jakata.validation-api,以及hibernate-validator的jar包,但是springboot项目中,spring-boot-starter-web 包中已经引入了 hibernate-validator 6.0.17.RELEASE了。

在这里插入图片描述



3.2 对需要验证的字段进行约束

级联验证用valid,复杂验证用正则表达式

给一个正则表达式的地址:https://www.w3cschool.cn/zhengzebiaodashi/regexp-metachar.html

package cn.iocoder.springboot.lab22.ex.validation.domain;

import org.hibernate.validator.constraints.Length;

import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import java.util.Date;

//javax.validation.constraints包下所支持的一些约束类型

/**
 * 员工信息
 * @author galen
 * @date 2020/2/26
 * */
public class Employee {
    @NotNull(message = "姓名必填!")
    @Length(max = 20, message = "姓名过长!")
    private String name;

    @NotNull(message = "工牌必填!")
    @Pattern(regexp = "^\\d{10}",message = "请输入10位数字工牌!")//长度10,0-9
    private String badgeCode;

    @Pattern(regexp = "^[1-2]",message = "性别参数错误!")
    @NotNull(message = "性别必填!")
    private String gender;

    @Past(message = "无效的出生日期!")
    private Date birthDate;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Email
    private String email;

    @NotNull(message = "身份证号码必填!")
    @Pattern(regexp = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$"
            ,message = "身份证号码格式错误")
    private String idCardNumber;

    @Valid
    private Salary salary;

    public Salary getSalary() {
        return salary;
    }

    public void setSalary(Salary salary) {
        this.salary = salary;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @NotNull(message = "联系电话必填!")
    @Pattern(regexp = "^[1]+\\d{10}$"
            ,message = "电话号码格式错误")
    private String phoneNumber;



    public String getIdCardNumber() {
        return idCardNumber;
    }

    public void setIdCardNumber(String idCardNumber) {
        this.idCardNumber = idCardNumber;
    }



    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBadgeCode() {
        return badgeCode;
    }

    public void setBadgeCode(String badgeCode) {
        this.badgeCode = badgeCode;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Date getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", badgeCode='" + badgeCode + '\'' +
                ", gender=" + gender +
                ", birthDate=" + birthDate +
                '}';
    }
}



3.3 添加自定义注解

package cn.iocoder.springboot.lab22.ex.validation.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author galen
 * @date 2020/2/26
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanValidation {
    String handler() default "default";
}



3.4 定义自定义注解的处理类

    /**
     * 定义环绕通知,在指定注解处(切点)拦截,获取切点对象参数,以及获取BeanValidation的自定义参数
     * */
    @Around("@annotation(beanValidation)")
    public final Object validateParamByAnnotation(ProceedingJoinPoint ponit, BeanValidation beanValidation) throws Throwable {
        log.info("==========================  start bean validation in :" + ponit.getSignature().getDeclaringTypeName() + "==========================");
        String errorMsg = packErrorMsg(ponit.getArgs());
        if (errorMsg != null && errorMsg.length() > 0) {
            String handlerName = beanValidation.handler();
            if ("default".equals(handlerName)) {
                //返回指定格式错误信息
                return R.error(errorMsg);
            }
            return customHandlerInvoke(handlerName, errorMsg);
        }
        return ponit.proceed();
    }



3.5 在需要验证的方法上加注解

/**
     * 调用BeanValidation 切面实现
     * @param
     * @return
     */
    @PostMapping("/validateByAspect")
    @BeanValidation
    public Object addEmployeeByAspect(@RequestBody Employee employee1){
        return "添加职员信息成功" ;
    }



3.6 效果演示

在这里插入图片描述



最后附上源码地址和参考地址


本文实战源码地址:SpringBoot-Labs

参考lab-22/lab-22-ex-validation-01章节。


重点推荐:芋道 Spring Boot 参数校验 Validation 入门

其他相关参考地址:

正则表达式:https://www.w3cschool.cn/zhengzebiaodashi/regexp-metachar.html

身份证正则:https://blog.csdn.net/loa_loa/article/details/81737086

表单验证通用切面:https://blog.csdn.net/weixin_43740223/article/details/100889250

验证拓展:自定义验证+分组验证:

https://www.cnblogs.com/beiyan/p/5946345.html

https://blog.csdn.net/Mr_rain/article/details/78247857

对hibernate注解进一步说明

https://blog.csdn.net/zalan01408980/article/details/79059434

https://www.cnblogs.com/lw5946/p/11574987.html

https://beanvalidation.org/

https://github.com/hibernate/hibernate-validator



版权声明:本文为xujinggen原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。