Loading

04-Spring(Boot) 整合 hibernate validator

零、使用场景

日常开发中,难免需要对参数进行一些参数正确性的校验,比如字段非空,字段长度限制,邮箱格式验证等等,可能普通操作就是写一些字段校验的代码去做处理判断,不同的地方可能会重复编写(不停搬砖~~),而这些校验出现在业务代码中,让我们的业务代码显得臃肿,并且不方便维护。

Hibernate Validator 框架刚好解决了这些问题,可以很优雅的方式实现参数的校验,让业务代码和校验逻辑 分开,不再编写重复的校验逻辑,从此在参数校验上不用花费太多时间。

一、Hibernate Validator 简介

Hibernate Validator是 Bean Validation 的参考实现 。 Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

二、常用注解

1、常用注解如下

验证注解 验证的数据类型 说明
@AssertFalse Boolean,boolean 验证注解的元素值是false javax.validation.constraints
@AssertTrue Boolean,boolean 验证注解的元素值是true javax.validation.constraints
@NotNull 任意类型 验证注解的元素值不是null javax.validation.constraints
@Null 任意类型 验证注解的元素值是null javax.validation.constraints
@Min(value=值) BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 验证注解的元素值大于等于@Min指定的value值 javax.validation.constraints
@Max(value=值) 和@Min要求一样 验证注解的元素值小于等于@Max指定的value值 javax.validation.constraints
@DecimalMin(value=值) 和@Min要求一样 验证注解的元素值大于等于@ DecimalMin指定的value值 javax.validation.constraints
@DecimalMax(value=值) 和@Min要求一样 验证注解的元素值小于等于@ DecimalMax指定的value值 javax.validation.constraints
@Digits(integer=整数位数, fraction=小数位数) 和@Min要求一样 验证注解的元素值的整数位数和小数位数上限 javax.validation.constraints
@Size(min=下限, max=上限) 字符串、Collection、Map、数组等 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 javax.validation.constraints
@NotBlank CharSequence子类型 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 javax.validation.constraints,org.hibernate.validator.constraints
@Length(min=下限, max=上限) CharSequence子类型 验证注解的元素值长度在min和max区间内 org.hibernate.validator.constraints
@NotEmpty CharSequence子类型、Collection、Map、数组 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) javax.validation.constraints,org.hibernate.validator.constraints
@Range(min=最小值, max=最大值) BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 验证注解的元素值在最小值和最大值之间 org.hibernate.validator.constraints
@Email(regexp=正则表达式,flag=标志的模式) CharSequence子类型(如String) 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 javax.validation.constraints,org.hibernate.validator.constraints
@Pattern(regexp=正则表达式,flag=标志的模式) String,任何CharSequence的子类型 验证注解的元素值与指定的正则表达式匹配 javax.validation.constraints
@Valid 任何非原子类型 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 javax.validation.constraints

1、对于基本数据类型,使用@Max、@Min等注解时,如果被注解元素为null,则该元素会被赋默认值,如int的默认值为0,boolean的默认值为false;

2、对于包装类型,使用@Max、@Min等注解时,如果被注解元素为null,则被注解元素就为null,因此当被注解元素不能为null时,则必须配合@NotNull等非空注解使用

3、对于字符类型和集合类型:@Length、@Size 仅判断被注解元素的长度/大小是否符合要求,不判断被注解元素是否为空,因此要保证被注解元素为空时必须与@NotNull等非空注解配合使用。

2、依赖包及版本关系

以上注解来源于两个jar包,一个是org.hibernate.hibernate-validator包,一个是javax.validation.validation-api,如下图所示,其中org.hibernate.hibernate-validator依赖于javax.validation.validation-api,可以看到,org.hibernate.hibernate-validator包下拥有自己的实现,如@Length、@Range、以及被标注为过时的@Email等。

image-20220316195738508

可以看到,两个依赖包都有一些共同的注解,在6.x.x的版本中,@Email、@NotBlank、@NotEmpty注解已经被标记为过时,因此不存在不知道引入那个版本的问题,而在5.x.x的版本中,这三个注解都未过时,因此必须引入org.hibernate.hibernate-validator下的注解,否则会报错

javax.validation.validation-api 3.0.0版本开始,包路径由javax.validation.constraints变为jakarta.validation.constraints.NotNull,如@NotNull注解的引用路径从javax.validation.constraints.NotNull 变为jakarta.validation.constraints.NotNull。具体原因可以搜索Jakarta EE,了解世事沧桑。

三、使用

1、包引入

  1. 可以直接引入org.hibernate.hibernate-validator包,需要指定具体版本

    <dependency>
    	<groupId>org.hibernate</groupId>
    	<artifactId>hibernate-validator</artifactId>
    	<version>6.0.0.Final</version>
    </dependency>
    
  2. 如果是spring boot项目,则推荐如下方式:通过引入spring-boot-starter-validation引入org.hibernate.hibernate-validator,无需指定版本,且由spring boot来保证版本间的兼容性。

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

2、基础使用

1、创建参数校验类

新建实体类:User

import java.util.List;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
// 如果org.hibernate.hibernate-validator的版本为5.x.x,则这里必须引入该包下的NotBlank和NotEmpty,否则会报错
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

@Data
public class User {

    @Max(value = 10, message = "最大不能超过10")
    private Integer i1;
    @Min(value = 20, message = "最小不能小于20")
    private Integer i2;
    @NotNull(message = "不能为null")
    @Min(value = 30, message = "最小不能小于30")
    @Max(value = 30, message = "最大不能超过30")
    private Integer i3;
    @Range(min = 40, max = 40, message = "值必须等于40")
    private int i4;

    @NotNull(message = "不能为null")
    private String s1;
    @NotBlank(message = "不能为空")
    private String s2;
    @NotEmpty(message = "不能为空")
    private String s3;
    @Length(min = 2, max = 5, message = "范围为2~5")
    private String s4;

    @NotNull(message = "不能null")
    private List<String> l1;
    @NotEmpty(message = "不能为空")
    private List<String> l2;
}

2、异常处理方式一

新建接口:ValidController

@RestController
public class ValidController {

    @GetMapping("valid1")
    public ResultBack user1(@Valid User user, BindingResult result) {
        if (result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            StringJoiner error = new StringJoiner(",");
            result.getFieldErrors().forEach(data -> error.add(data.getDefaultMessage()));
            return ResultBack.fail(error.toString());
        }
        System.out.println(user);
        return ResultBack.success(user);
    }

}

这里在接口参数中通过@Valid注解来标识该实体类需求被校验,并通过紧跟在@Valid注解参数后面的BindingResult对象接受错误信息,然后在方法中进行判断、封装处理,返回错误信息。

优点:关于参数校验的逻辑只有错误处理的一个if块代码,没有其他多余的校验逻辑,简洁明了。

缺点:对于每个需求校验的接口,都需要添加BindingResult参数,而且要保证参数位置紧跟在@Valid参数后面,另外每个接口都有一样的错误处理逻辑,重复编码,不方便维护。

3、异常处理方式二

通过全局异常的方式进行处理:

上面的代码中,如果不加BindingResult,则程序会抛出一个异常:org.springframework.validation.BindException,其实是org.springframework.validation.MethodArgumentNotValidExceptionMethodArgumentNotValidException继承于BindException。因此,这里可以用全局异常处理的方式,进行统一处理,就不用每个校验接口都加BindingResult参数和错误处理逻辑了。

完整的代码如下:

@RestControllerAdvice
public class GlobalException {

    @ExceptionHandler({BindException.class})
    @ResponseStatus(HttpStatus.OK)
    public ResultBack handleBindException(BindException ex) {
        BasicLogUtil.info("BindException");

        StringBuilder errorMsg = new StringBuilder();
        // 获取所有字段验证出错的信息
        List<FieldError> allErrors = ex.getFieldErrors();
        allErrors.forEach(fieldError -> errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("; "));
        return new ResultBack(ResultStatus.FAILURE, errorMsg.toString());
    }

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    public ResultBack handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BasicLogUtil.info("MethodArgumentNotValidException");

        StringBuilder errorMsg = new StringBuilder();
        // 获取所有字段验证出错的信息
        List<FieldError> allErrors = ex.getFieldErrors();
        allErrors.forEach(fieldError -> errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("; "));
        return new ResultBack(ResultStatus.FAILURE, errorMsg.toString());
    }

}

这里对两个异常分别做了处理,可以看到,处理方式都是一样的,只是第一行打印输出的内容不同,从而判断出这里到底抛出的是那个异常。有兴趣的同学可以试一下,只有注释掉handleMethodArgumentNotValidException方法时,控制台才打印BindException,从此可以推断这里抛出的异常是MethodArgumentNotValidException

完整项目代码详见:OMaster/valid (gitee.com)

posted @ 2022-03-16 21:54  OMaster  阅读(1408)  评论(0编辑  收藏  举报