使用Hibernate-Validator框架优雅的校验参数
简介
项目中,难免需要对参数进行一些参数正确性的校验,这些小样出现在业务代码中,多次出现if校验数据使得业务代码显得臃肿,所以Hibernate validator框架刚好解决这些问题,可以很优雅的方式实现参数的校验,让业务代码和小样逻辑分开,不再编写重复的校验逻辑。hibernate Validator提供了JSR303规范中所有内置约束的实现,除此之外还有一些附加约束。
Bean Validator为JavaBean验证定义了相关数据模型和API。缺省的元数据是Java Annotations,通过XML可以对原有的元数据信息进行覆盖和扩展,Bean validation是一个运行时数据验证框架,在验证之后的验证的错误信息会被马上返回。
作用
- 验证逻辑与业务之间进行了分离,降低了程序耦合度;
- 统一且规范的验证方式,无需你再次编写重复的验证代码;
- 开发人员更加专注于自己的业务,将这些繁琐的事情统统丢一边;
使用方法
pom依赖
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
如果项目的框架是 spring boot 的话,在 spring-boot-starter-web 中已经包含了 Hibernate-validator 的依赖。
常见注解
来自 javax.validation.constraints 的注解
//被注释的元素,值必须是一个字符串,不能为null,且调用trim()后,长度必须大于0
@NotBlank(message = "")
//被注释的元素,值不能为null,但可以为"空",用于基本数据类型的非空校验上,而且被其标注的字段可以使用 @size、@Max、@Min 等对字段数值进行大小的控制
@NotNull(message = "")
//被注释的的元素,值不能为null,且长度必须大于0,一般用String、Collection、Map、Array
@NotEmpty(message = "")
//被注释的元素必须符合指定的正则表达式。
@Pattern(regexp = "", message = "")
//被注释的元素的大小必须在指定的范围内。限制字符长度或集合容量。
@Size(min =, max =)
//被注释的元素,值必须是一个数字,且值必须大于等于指定的最小值
@Min(value = long以内的值, message = "")
//被注释的元素,值必须是一个数字,且值必须小于等于指定的最大值
@Max(value = long以内的值, message = "")
//被注释的元素,值必须是一个数字,其值必须大于等于指定的最小值
@DecimalMin(value = 可以是小数, message = "")
//被注释的元素,值必须是一个数字,其值必须小于等于指定的最大值
@DecimalMax(value = 可以是小数, message = "")
//被注释的元素,值必须为null
@Null(message = "")
//被注释的元素必须是一个数字,其值必须在可接受的范围内
@Digits(integer =, fraction =)
//被注释的元素,值必须为true
@AssertTrue(message = "")
//被注释的元素,值必须为false
@AssertFalse(message = "")
//被注释的元素必须是一个过去的日期
@Past(message = "")
//被注释的元素必须是一个将来的日期
@Future(message = "")
//被注释的元素必须是电子邮件地址
@Email(regexp = "", message = "")
//被注释的元素,值必须是一个数字,且是一个正数
@Positive(message = "")
//被注释的元素,值必须是一个数字,且是一个正数或零
@PositiveOrZero(message = "")
//被注释的元素,值必须是一个数字,且是一个负数
@Negative(message = "")
//被注释的元素,值必须是一个数字,且是一个负数或零
@NegativeOrZero(message = "")
来自 org.hibernate.validator.constraints 的注解
//字符串长度
@Length(min = , max = )
//数值范围,必须是Number或值为数值的 String
@Range(min = , max = )
//集合中每个元素唯一
@UniqueElements(message = "")
//检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
@URL(message = "", protocol = , host = , port = )
//字符串必须是信用号(按美国标准验的)
@CreditCardNumber(message = "")
//字符串是安全的html
@SafeHtml(message = "")
创建自定义校验器
示例1:创建自定义注解,用于判断年龄是否符合约束
/**
* 性别约束
*/
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SexConstraintValidator.class)
public @interface Sex {
String message() default "性别有误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- @Target用于指定使用范围
- @Retention(RetentionPolicy.RUNTIME)表示注解在运行时可以通过反射获取到
- @Constraint(validatedBy = xxx.class)指定该注解校验逻辑
判断注解的值是否符合约束
/**
* 性别约束逻辑判断
*/
public class SexConstraintValidator implements ConstraintValidator<Sex, String> {
public void initialize(Sex constraintAnnotation) {
}
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && (value.equals("男") || value.equals("女"));
}
}
然后,直接在实体类的字段上使用
@Sex
private String sex;
示例2:自定义校验器校验参数为指定值
自定义一个注解@EnumValue
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumScopeValueValidator.class})
public @interface EnumScopeValue {
/**
* 默认错误消息
* @return
*/
String message() default "必须为指定值";
String[] strValues() default {};
int[] intValues() default {};
/**
* 分组 将validator进行分类,不同的类group中会执行不同的validator操作
* @return
*/
Class<?>[] groups() default {};
/**
* 负载 主要是针对bean,很少使用
* @return
*/
Class<? extends Payload>[] payload() default {};
/**
* 指定多个时使用
*/
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumScopeValue[] value();
}
}
定义一个校验类来写校验的逻辑
/**
* @author linhongwei
* @ClassName EnumValueValidator.java
* @Description 自定义校验器
*/
public class EnumScopeValueValidator implements ConstraintValidator<EnumScopeValue, Object> {
private String[] strValues;
private int[] intValues;
@Override
public void initialize(EnumScopeValue constraintAnnotation) {
strValues = constraintAnnotation.strValues();
intValues = constraintAnnotation.intValues();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value instanceof String) {
for (String s : strValues) {
if (s.equals(value)) {
return true;
}
}
} else if (value instanceof Integer) {
for (int s : intValues) {
if (s == ((Integer) value).intValue()) {
return true;
}
}
}
return false;
}
}
分组校验
有这样一种场景,新增用户信息的时候,不需要验证userId不为空(因为id为系统后台生成);修改的时候需要验证userId不为空,这时候可用到validator的分组验证功能。
以CURD为例,我们先创建两个标记接口:
/**
* @author linhongwei
* @version 1.0.0
* @ClassName OnUpdate.java
* @Description 修改时要验证的规则
* @createTime 2021年07月01日 22:11:00
*/
public interface OnUpdate {
}
/**
* @author linhongwei
* @version 1.0.0
* @ClassName OnCreate.java
* @Description 新增时要验证的规则
* @createTime 2021年07月01日 22:11:00
*/
public interface OnCreate {
}
然后在我们要校验的实体Bean中进行标记
@Data
public class SysUser {
/**
* 更新时用户ID不能为空
*/
@NotNull(message = "用户id不能为空", groups = OnUpdate.class)
private Integer userId;
/**
* 新增、修改用户名都不能为空
*/
@NotEmpty(message = "用户名不能为空", groups = {OnCreate.class, OnUpdate.class})
private String userName;
}
最后在真正要校验的时候指定要校验的分组即可(@Validated指定分组或者手动校验指定分组)。
自定义组序列分组校验
据对象状态来重定义默认组序列,我在工作中遇到一个参数校验问题,比如需要根据一个参数值X来校验其他参数是否符合要求,要满足X的不同值都能达到校验效果,我实现了DefaultGroupSequenceProvider接口重定义了校验组序列
实现DefaultGroupSequenceProvider
public class TestGroupSequenceProvider implements DefaultGroupSequenceProvider<EntityDTO> {
@Override
public List<Class<?>> getValidationGroups(EntityDTO entityDTO) {
List<Class<?>> defaultGroupSequence = new ArrayList<>();
//这一步不能省略,否则会抛错
defaultGroupSequence.add(EntityDTO.class);
if (entityDTO != null) {
if ("0".equals(entityDTO.getImportType())) {
defaultGroupSequence.add(NoGroup.class);
} else {
defaultGroupSequence.add(YesGroup.class);
}
}
return defaultGroupSequence;
}
}
在参数类上添加注解@GroupSequenceProvider
@Data
@GroupSequenceProvider(value = TestGroupSequenceProvider.class)
public class EntityDTO {
@NotBlank(message = "导入类型不能为空")
private String importType;
@NotBlank(message = "姓名不能为空", groups = {YesGroup.class})
private String name;
}
指定分组
public interface YesGroup {
}
public interface NoGroup {
}
@Validated和@Valid的区别
@Validated是Spring Validation验证框架对参数的验证机制所使用的注解,使用的是Spring的 JSR-303规范,它是标准JSR-303规范的一种变种
需要引入的注解包为: import org.springframework.validation.annotation.Validated;
@Valid是由javax提供的,使用的是标准的JSR-303规范
需要引入的注解包为:import javax.validation.Valid;
此外,@Valid配合BindingResult可以直接将参数校验的结果按照自定义格式输出到我们想要的位置。
@Validated和@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
1)分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
2)注解的地方
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上。
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上。
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
3)嵌套验证
首先我们要了解什么事嵌套。
比如我们现在有个实体叫做Item:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Prop> props;
}
Item带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。
现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:
@RestController
public class ItemController {
@RequestMapping("/item/add")
public void addItem(@Validated Item item, BindingResult bindingResult) {
doSomething();
}
}
在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List<Prop>中有Prop的pid为空或者是负数,入参验证不会检测出来
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来,bindingResult就会记录相应的错误。
工具类手动调用校验返回结果
除了引入hibernate-validator依赖外,也用到下面两个依赖:
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
验证器工厂:ValidatorFactory
创建验证器工厂的方式有这么几种:
private static ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
//Hibernate Validator有以下两种验证模式:
//failFast为false, 普通模式(会校验完所有的属性,然后返回所有的验证失败信息),
//failFast为true, 快速失败返回模式,只要有一个验证失败,则返回
// private static ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
// .configure().failFast(true).buildValidatorFactory();
验证器:Validator
通过验证器工厂可以得到验证器
Validator validator = validatorFactory.getValidator();
完整的工具类
/**
* bean Validator 校验类
*
* @author linhongwei
*/
public final class BeanValidator {
private static ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
private BeanValidator() {
}
/**
* 校验, 可分组, 返回所有的提示信息
*
* @param t
* @param groups
* @param <T>
* @return
*/
public static <T> Map<String, String> getMapValidate(T t, Class... groups) {
Set validateResult = getConstraintViolationSet(t, groups);
//如果校验有值
if (validateResult.isEmpty()) {
return Collections.emptyMap();
} else {
LinkedHashMap errors = new LinkedHashMap();
Iterator iterator = validateResult.iterator();
while (iterator.hasNext()) {
ConstraintViolation violation = (ConstraintViolation) iterator.next();
errors.put(violation.getPropertyPath().toString(), violation.getMessage());
}
return errors;
}
}
/**
* 校验, 可分组, 返回所有的提示信息
*
* @param t
* @param groups
* @param <T>
* @return
*/
public static <T> List<ItemResult> getObjectValidate(T t, Class... groups) {
Set validateResult = getConstraintViolationSet(t, groups);
//如果校验有值
if (validateResult.isEmpty()) {
return new ArrayList();
} else {
List<ItemResult> validItems = new ArrayList();
Iterator iterator = validateResult.iterator();
while (iterator.hasNext()) {
ConstraintViolation violation = (ConstraintViolation) iterator.next();
validItems.add(new ItemResult(String.valueOf(violation.getPropertyPath()), violation.getMessage()));
}
return validItems;
}
}
/**
* 校验对象,可分组, 返回第一个字段校验的提示信息
*
* @param t
* @param groups
* @param <T>
* @return
*/
public static <T> ItemResult getFirstFieldValidate(T t, Class... groups) {
Set<ConstraintViolation<T>> validateSet = getConstraintViolationSet(t, groups);
//如果校验有值
if (!validateSet.isEmpty()) {
Iterator iterator = validateSet.iterator();
while (iterator.hasNext()) {
ConstraintViolation violation = (ConstraintViolation) iterator.next();
return new ItemResult(String.valueOf(violation.getPropertyPath()), violation.getMessage());
}
}
return null;
}
/**
* 校验对象,可分组, 提示信息都连接起来
*
* @param t
* @param groups
* @param <T>
* @return
*/
public static <T> String getStringValidate(T t, Class... groups) {
Set<ConstraintViolation<T>> validateSet = getConstraintViolationSet(t, groups);
String messages = "";
//如果校验有值
if (!validateSet.isEmpty()) {
messages = validateSet.stream()
.map(ConstraintViolation::getMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("参数输入有误!");
}
return messages;
}
/**
* 校验多个对象, 不分组
*
* @param collection
* @return
*/
public static Map<String, String> getMapValidateList(Collection<?> collection) {
//判断是否为空
Preconditions.checkNotNull(collection);
Map errors = new LinkedHashMap();
for (Object o : collection) {
Map result = getMapValidate(o, new Class[0]);
errors.putAll(result);
}
return errors;
}
/**
* 校验多个对象, 不分组
*
* @param collection
* @return
*/
public static List<ItemResult> getObjectValidateList(Collection<?> collection) {
//判断是否为空
Preconditions.checkNotNull(collection);
List<ItemResult> allItems = new ArrayList();
for (Object o : collection) {
List<ItemResult> items = getObjectValidate(o, new Class[0]);
allItems.addAll(items);
}
return allItems;
}
/**
* 至少校验一个对象,不分组
*
* @param first
* @param objects
* @return
*/
public static Map<String, String> getMapValidateObject(Object first, Object... objects) {
if (objects != null && objects.length > 0) {
return getMapValidateList(Lists.asList(first, objects));
} else {
return getMapValidate(first, new Class[0]);
}
}
/**
* 至少校验一个对象,不分组
*
* @param first
* @param objects
* @return
*/
public static List<ItemResult> getObjectValidateObject(Object first, Object... objects) {
if (objects != null && objects.length > 0) {
return getObjectValidateList(Lists.asList(first, objects));
} else {
return getObjectValidate(first, new Class[0]);
}
}
/**
* 获取前四个的校验结果抛出异常
*
* @param t
* @param groups
* @param <T>
*/
public static <T> void checkValidResultFourLimit(T t, Class... groups) {
Set<ConstraintViolation<T>> validateSet = getConstraintViolationSet(t, groups);
if (!validateSet.isEmpty()) {
List<ConstraintViolation<T>> arrayList = new ArrayList(validateSet);
if (arrayList.size() > 4) {
arrayList = arrayList.subList(0, 4);
}
String messages = arrayList.stream()
.map(ConstraintViolation::getMessage)
.reduce((m1, m2) -> m1 + ";" + m2)
.orElse("参数输入有误!");
throw new ProjectException(messages, CodeMsg.BIND_ERROR.getCode());
}
}
/**
* 获取校验中的一个结果抛出异常
*
* @param t
* @param groups
* @param <T>
*/
public static <T> void checkSingleValidResult(T t, Class... groups) {
ItemResult itemResult = getFirstFieldValidate(t, groups);
if (null != itemResult) {
throw new ProjectException(itemResult.getValue(), CodeMsg.BIND_ERROR.getCode());
}
}
/**
* 获取所有的校验结果抛出异常
*
* @param t
* @param groups
* @param <T>
*/
public static <T> void checkAllValidResult(T t, Class... groups) {
String message = getStringValidate(t, groups);
if (StringUtils.isNotBlank(message)) {
throw new ProjectException(message, CodeMsg.BIND_ERROR.getCode());
}
}
public static class ItemResult {
private String name;
private String value;
public ItemResult(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
private static <T> Set getConstraintViolationSet(T t, Class... groups) {
Validator validator = validatorFactory.getValidator();
return validator.validate(t, groups);
}
}
总结


浙公网安备 33010602011771号