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等。
可以看到,两个依赖包都有一些共同的注解,在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、包引入
-
可以直接引入
org.hibernate.hibernate-validator
包,需要指定具体版本<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.0.Final</version> </dependency>
-
如果是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.MethodArgumentNotValidException
,MethodArgumentNotValidException
继承于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)