Hibernate数据校验简介

本文介绍了JSR303和349数据校验规范,包括BeanValidator框架、常用注解及其功能。接着,探讨了Hibernate在数据校验中的扩展,如新增注解和实现细节。通过示例展示了如何使用HibernateValidator进行数据校验,以及自定义ConstraintValidator的步骤。此外,还涵盖了Validator的特性,如约束级别、集合约束、类级别约束、可继承性、递归校验、方法参数和返回值校验、分组校验以及Payload的使用。了解这些概念和实践,能帮助开发者更高效地进行数据校验。
摘要由CSDN通过智能技术生成

我们在业务中经常会遇到参数校验问题,比如前端参数校验、Kafka消息参数校验等,如果业务逻辑比较复杂,各种实体比较多的时候,我们通过代码对这些数据一一校验,会出现大量的重复代码以及和主要业务无关的逻辑。Spring MVC提供了参数校验机制,但是其底层还是通过Hibernate进行数据校验,所以有必要去了解一下Hibernate数据校验和JSR数据校验规范。
JSR数据校验规范

Java官方先后发布了JSR303与JSR349提出了数据合法性校验提供的标准框架:BeanValidator,BeanValidator框架中,用户通过在Bean的属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
JSR注解列表

JSR标准中的数据校验注解如下所示:
在这里插入图片描述

JSR注解内容

我们以常用的比较简单的@NotNull注解为例,看看注解中都包含那些内容,如下边的源码所示,可以看到@NotNull注解包含以下几个内容:

1.message:错误消息,示例中的是错误码,可以根据国际化翻译成不同的语言。
2.groups: 分组校验,不同的分组可以有不同的校验条件,比如同一个DTO用于create和update时校验条件可能不一样。
3. payload:BeanValidation API的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
	String message() default "{javax.validation.constraints.NotNull.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	/**
	 * Defines several {@link NotNull} annotations on the same element.
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	@interface List {

		NotNull[] value();
	}
}

错误消息message、分组group这些功能我们程序中使用比较多,在我介绍Spring Validator数据校验的文章中有详细说明,但是关于payload我们接触的比较少,下面我们举例说明以下payload的使用,下面的示例中,我们用payload来标识数据校验失败的严重性,通过以下代码。在校验完一个ContactDetails的示例之后, 你就可以通过调用ConstraintViolation.getConstraintDescriptor().getPayload()来得到之前指定到错误级别了,并且可以根据这个信息来决定接下来到行为.

public class Severity {
    public static class Info extends Payload {};
    public static class Error extends Payload {};
}

public class ContactDetails {
    @NotNull(message="Name is mandatory", payload=Severity.Error.class)
    private String name;

    @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
    private String phoneNumber;

    // ...
}

JSR校验接口

通过前面的JSR校验注解,我们可以给某个类的对应字段添加校验条件,那么怎么去校验这些校验条件呢?JSR进行数据校验的核心接口是Validation,该接口的定义如下所示,我们使用比较多的接口应该是 Set<ConstraintViolation> validate(T object, Class<?>… groups);,该方法可以用于校验某个Object是否符合指定分组的校验规则,如果不指定分组,那么只有默认分组的校验规则会生效。

public interface Validator {

	/**
	 * Validates all constraints on {@code object}.
	 */
	<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);

	/**
	 * Validates all constraints placed on the property of {@code object}
	 * named {@code propertyName}.
	 */
	<T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName,Class<?>... groups);

	/**
	 * Validates all constraints placed on the property named {@code propertyName}
	 * of the class {@code beanType} would the property value be {@code value}.
	 */
	<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups);

	/**
	 * Returns the descriptor object describing bean constraints.
	 * The returned object (and associated objects including
	 * {@link ConstraintDescriptor}s) are immutable.
	 */
	BeanDescriptor getConstraintsForClass(Class<?> clazz);

	/**
	 * Returns an instance of the specified type allowing access to
	 * provider-specific APIs.
	 * <p>
	 * If the Jakarta Bean Validation provider implementation does not support
	 * the specified class, {@link ValidationException} is thrown.call
	 */
	<T> T unwrap(Class<T> type);

	/**
	 * Returns the contract for validating parameters and return values of methods
	 * and constructors.
	 */
	ExecutableValidator forExecutables();
}

Hibernate数据校验

基于JSR数据校验规范,Hibernate添加了一些新的注解校验,然后实现了JSR的Validator接口用于数据校验。
Hibernate新增注解
在这里插入图片描述

Hibiernate数据校验

如何使用Hibernate进行数据校验呢?我们知道JSR规定了数据校验的接口Validator,Hibernate用ValidatorImpl类中实现了Validator接口,我们可以通过Hibernate提供的工厂类HibernateValidator.buildValidatorFactory创建一个ValidatorImpl实例。使用Hibernate创建一个Validator实例的代码如下所示。

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
    .configure()
    .addProperty( "hibernate.validator.fail_fast", "true" )
    .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

Hibernate校验源码

通过上面的内容,我们知道Hibernate可以用工厂方法实例化一个Validator接口的实例,这个实例可以用于带有校验注解的校验JavaBean,那么Hibernate底层是如何实现这些校验逻辑的呢?我们以如下JavaBean为例,解析Hibernate校验的源码。

@Data
public class Person {

    @NotBlank
    @Size(max=64)
    private String name;

    @Min(0)
    @Max(200)
    private int age;
}

ConstraintValidator介绍

ConstraintValidator是Hibernate中数据校验的最细粒度,他可以校验指定注解和类型的数值是否合法。比如上面例子中的@Max(200)private int age;,对于age字段的校验就会使用一个叫MaxValidatorForInteger的ConstraintValidator,这个ConstraintValidator在校验的时候会判断指定的数值是不是大于指定的最大值。

public class MaxValidatorForInteger extends AbstractMaxValidator<Integer> {

	@Override
	protected int compare(Integer number) {
		return NumberComparatorHelper.compare( number.longValue(), maxValue );
	}
}

public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> {

	protected long maxValue;

	@Override
	public void initialize(Max maxValue) {
		this.maxValue = maxValue.value();
	}

	@Override
	public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
		// null values are valid
		if ( value == null ) {
			return true;
		}

		return compare( value ) <= 0;
	}

	protected abstract int compare(T number);
}

ConstraintValidator初始化

我们在前面的内容中说到Hibernate提供了ValidatorImpl用于数据校验,那么ValidatorImpl和ConstraintValidator是什么关系呢,简单来说就是ValidatorImpl在初始化的时候会初始化所有的ConstraintValidator,在校验数据的过程中调用这些内置的ConstraintValidator校验数据。内置ConstraintValidator的对应注解的@Constraint(validatedBy = { })是空的。

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { }) // 这儿是空的
public @interface AssertFalse {

	String message() default "{javax.validation.constraints.AssertFalse.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	/**
	 * Defines several {@link AssertFalse} annotations on the same element.
	 *
	 * @see javax.validation.constraints.AssertFalse
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	@interface List {

		AssertFalse[] value();
	}
}

自定义ConstraintValidator

如果Hibernate和JSR中的注解不够我用,我需要自定义一个注解和约束条件,我们应该怎么实现呢。实现一个自定义校验逻辑一共分两步:1.注解的实现。2.校验逻辑的实现。比如我们需要一个校验字段状态的注解,我们可以使用以下示例定义一个注解:

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = StatusValidator.class)
@Documented
public @interface ValidStatus {
    String message() default "状态错误 ";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    /**
     * 有效的状态值集合,默认{1,2}
     */
    int[] value() default {1,2};
}

实现了注解之后,我们需要实现注解中的@Constraint(validatedBy = StatusValidator.class),示例代码如下:

/**
 * 校验状态是否属于指定状态集
 (ConstraintValidator后指定的泛型对象类型为
 注解类和注解注释的字段类型<ValidStatus, Integer>)
 */
public class StatusValidator implements ConstraintValidator<ValidStatus, Integer> {
    private Integer[] validStatus;

    @Override
    public void initialize(ValidStatus validStatus) {
        int[] ints = validStatus.value();
        int n = ints.length;
        Integer[] integers = new Integer[n];
        for (int i = 0; i < n; i++) {
            integers[i] = ints[i];
        }
        this.validStatus = integers;
    }

    @Override
    public boolean isValid(Integer n, ConstraintValidatorContext constraintValidatorContext) {
        List<Integer> status = Arrays.asList(validStatus);
        if (status.contains(n)) {
            return true;
        }
        return false;
    }
}

Validator的特性
四种约束级别
成员变量级别的约束

约束可以通过注解一个类的成员变量来表达。如下代码所示:

@Data
public class Person {

    @NotBlank
    @Size(max=64)
    private String name;

    @Min(0)
    @Max(200)
    private int age;
}

属性约束

如果你的模型类遵循javabean的标准,它也可能注解这个bean的属性而不是它的成员变量。关于JavaBean的介绍可以看我的另外一篇博客。

@Data
public class Person {

    private String name;

    @Min(0)
    @Max(200)
    private int age;

    @NotBlank
    @Size(max=64)
    public String getName(){
        return name;
    }
}

集合约束

通过在约束注解的@Target注解在约束定义中指定ElementType.TYPE_USE,就可以实现对容器内元素进行约束
类级别约束

一个约束被放到类级别上,在这种情况下,被验证的对象不是简单的一个属性,而是一个完整的对象。使用类级别约束,可以验证对象几个属性之间的相关性,比如不允许所有字段同时为null等。

@Data
@NotAllFieldNull
public class Person {

    private String name;

    @Min(0)
    @Max(200)
    private int age;

    @NotBlank
    @Size(max=64)
    public String getName(){
        return name;
    }
}

校验注解的可继承性

父类中添加了约束的字段,子类在进行校验时也会校验父类中的字段。
递归校验

假设我们上面例子中的Person多了一个Address类型的字段,并且Address也有自己的校验,我们怎么校验Address中的字段呢?可以通过在Address上添加@Valid注解实现递归校验。

@Data
public class Person {

    private String name;

    @Min(0)
    @Max(200)
    private int age;

    @Valid
    public Address address;
}

@Data
public class Address{

    @NotNull
    private string city;
}

方法参数校验

我们可以通过在方法参数中添加校验注解,实现方法级别的参数校验,当然这些注解的生效需要通过一些AOP实现(比如Spring的方法参数校验)。

public void createPerson(@NotNull String name,@NotNull Integer age){

}

方法参数交叉校验

方法也支持参数之间的校验,比如如下注解不允许创建用户时候用户名和年龄同时为空,注解校验逻辑需要自己实现。交叉校验的参数是Object[]类型,不同参数位置对应不同的Obj。

@NotAllPersonFieldNull
public void createPerson( String name,Integer age){

}

方法返回值校验

public @NotNull Person getPerson( String name,Integer age){
    return null;
}

分组功能

我在另一篇介绍Spring校验注解的文章中说过,在Spring的校验体系中,@Valid注解不支持分组校验,@Validated注解支持分组校验。 事实上这并不是JSR注解中的@Valid不支持分组校验,而是Spring层面把@Valid注解的分组校验功能屏蔽了。

所以原生的JSR注解和Hibernate校验都支持分组校验功能,具体校验逻辑可以参考我有关Spring数据校验的文章。
分组继承

我们知道JSR分组校验功能是使用注解中的group字段,group字段存储了分组的类别,那么如果分组的类之间有继承关系,分组校验会被继承吗?答案是会的。
分组顺序

如果我们在校验的过程中需要指定校验顺序,那么我们可以给校验条件分组,分组之后就会按照顺序校验对象中的各个属性。

GroupSequence({ Default.class, BaseCheck.class, AdvanceCheck.class })
public interface OrderedChecks {
}
Payload

如果我们需要在不同的情况下有不同的校验方式,比如中英文环境之类的,这种时候用分组就不是很合适了,可以考虑使用PayLoad。用户可以在初始化Validator时候指定当前环境的payload,然后在校验环节拿到环境中的payload走不同的校验流程:

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .constraintValidatorPayload( "US" )
        .buildValidatorFactory();

Validator validator = validatorFactory.getValidator();

public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {

    public String countryCode;

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        boolean isValid = false;

        String countryCode = constraintContext
                .unwrap( HibernateConstraintValidatorContext.class )
                .getConstraintValidatorPayload( String.class );

        if ( "US".equals( countryCode ) ) {
            // checks specific to the United States
        }
        else if ( "FR".equals( countryCode ) ) {
            // checks specific to France
        }
        else {
            // ...
        }

        return isValid;
    }
}

只要一步一个脚印,水滴石穿,吃透、搞懂、拿捏住是完全没有问题的!看到这里的都是妥妥的铁粉无疑了,底下是我微信找到我的可是有大把源码,学习路线思维导图啥的,多的我就不透露,539413949看大家自己的积极性了啊,热爱所热爱的, 学习伴随终生

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值