使用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);
    }
}

总结

 

posted @ 2022-01-06 13:17  残城碎梦  阅读(1447)  评论(0)    收藏  举报