今天我们将讨论Hibernate Validator以及如何以完全自包含的方式提供您自己的约束和/或验证器。也就是说,将其打包到自己的JAR文件中,以便其他人可以通过将其添加到类路径来使用您的库。

此功能基于Hibernate Validator对Java的ServiceLoader机制的使用,该机制允许注册额外的约束定义。但关于细节稍后再说。

在现实生活中,构建自己的具有约束的库并共享它的场景可能是什么?好吧,让我们假设你正在构建一个可能需要验证的数据类库。由于难以跟踪所有这样的库并为他们编写/维护所有这些约束,Hibernate Validator为这些库的作者提供了一个编写和共享他们自己的验证扩展的机会。这些扩展可以被Hibernate Validator拾取并用于验证你的数据类。

此外,此ServiceLoader机制还可以解决另一个问题。由于你试图成为一个好开发者,只为库的最终用户提供相关的类,并隐藏实现细节,你可能不想通过在@Constraint注解的validatedBy()参数中提及它来公开你的验证器实现。通过使用本文中描述的方法,你可以实现所有这些。

对于我们的示例,我们将创建两个模块的Maven项目 - 一个将包含验证器,代表可以共享的“库”,另一个模块将是该库的消费者,并包含一些测试。

别再说了,让我们验证一些bean!这就是我们聚集在这里的原因,对吧? :)

使用自定义注解和验证器

首先,让我们考虑添加自己的约束注解和相应的验证器的情况。

时间,它需要时间……

虽然Hibernate Validator 5.4支持Java 8的广泛日期/时间API(Bean Validation 2.0将支持提升到规范级别),但仍有一些类型不受支持,其中之一就是Duration。这种类型并不描述一个时间点,因此常规的日期/时间约束(@Future / @Past)对它来说没有意义。因此,如果我们想验证一个给定的持续时间具有指定的最小长度,就需要一个新的约束。我们可以称它为@DurationMin

我们新的约束注解可能看起来是这样的

DurationMin.java
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@ReportAsSingleViolation
public @interface DurationMin {

    String message() default "{com.acme.validation.constraints.DurationMin.message}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };

    long value() default 0;
    ChronoUnit units() default ChronoUnit.NANOS;

    /**
     * Defines several {@code @DurationMin} annotations on the same element.
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        DurationMin[] value();
    }
}

现在我们已经有了注解,我们需要创建相应的约束验证器。为此,你需要实现ConstraintValidator接口,它包含两个方法

  • initialize() - 根据注解参数初始化验证器

  • isValid() - 执行实际验证

一个实现可能如下所示

DurationMinValidator.java
public class DurationMinValidator implements ConstraintValidator<DurationMin, Duration> {

    private Duration duration;

    @Override
    public void initialize(DurationMin constraintAnnotation) {
        this.duration = Duration.of( constraintAnnotation.value(), constraintAnnotation.units() );
    }

    @Override
    public boolean isValid(Duration value, ConstraintValidatorContext context) {
        // null values are valid
        if ( value == null ) {
            return true;
        }
        return duration.compareTo( value ) < 1;
    }
}

由于我们正在创建一个新的约束注解,我们还应该为它提供一个默认消息。这可以通过在类路径中放置一个ContributorValidationMessages.properties属性文件来完成。此属性文件应包含一个键/消息对,其中键是注解声明中使用的键(在我们的情况下是com.acme.validation.constraints.DurationMin.message),而消息是当验证失败时你想显示的消息。我们的属性文件如下所示

ContributorValidationMessages.properties
com.acme.validation.constraints.DurationMin.message = must be greater than or equal to {value} {units}

当标准的ValidationMessages包不包含给定的消息键时,Hibernate Validator会查询ContributorValidationMessages包,允许库作者将其约束的默认消息作为其JAR的一部分提供。

如果你什么都不做,你的约束注解将独立于验证器的存在而存在。Hibernate Validator也不会知道验证器。因此,为了确保Hibernate Validator发现你的DurationMinValidator,你需要创建一个名为META-INF/services/javax.validation.ConstraintValidator的文件,并将验证器实现的完全限定名放入其中

META-INF/services/javax.validation.ConstraintValidator
com.acme.validation.validators.DurationMinValidator

完成所有这些后,你的新约束注解可以在Duration元素上使用,如下所示

Task.java
public class Task {

    private String taskName;
    @DurationMin(value = 2, units = ChronoUnit.HOURS)
    private Duration timeSpent;

    public Task(String taskName, Duration timeSpent) {
        this.taskName = taskName;
        this.timeSpent = timeSpent;
    }
}

项目结构应类似于以下内容

project structure, align=

这里展示的整个源代码可以在GitHub上的hibernate-demos存储库中找到。

使用标准约束对非标准类进行验证

现在让我们考虑你想要一个标准的Bean Validation约束支持一些其他类型的情况,而不仅仅是已经支持的类型。

ThreeTen Extra类型验证

由于我们讨论了与日期/时间相关的验证,所以让我们在这个示例中也保持相同的主题。在本节中,我们将查看ThreeTen Extra类型 - 一个提供额外日期和时间类的优秀库,以补充Java中已存在的那些。

Bean Validation通过@Past/@Future注解提供了对时间类型的验证支持。因此,我们希望也在ThreeTen Extra类型上使用这些注解。为了使这个例子简单,我们将只为YearWeekYearQuarter提供验证器。

让我们从实现ConstraintValidator接口开始

FutureYearWeekValidator.java
public class FutureYearWeekValidator implements ConstraintValidator<Future, YearWeek> {

    @Override
    public void initialize(Future constraintAnnotation) {
    }

    public boolean isValid(YearWeek value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }
        return YearWeek.now().isBefore( value );
    }
}

下一步是在META-INF/services/javax.validation.ConstraintValidator文件中提供实现的验证器列表

META-INF/services/javax.validation.ConstraintValidator
com.acme.validation.validators.FutureYearQuarterValidator
com.acme.validation.validators.FutureYearWeekValidator
com.acme.validation.validators.PastYearQuarterValidator
com.acme.validation.validators.PastYearWeekValidator

之后,我们可以将其打包成一个 JAR 文件,并准备好使用我们的验证器与全世界分享它们!

最终,我们的项目结构应该看起来像这样

project structure, align=

现在您可以将 @Past@Future 注解放置在 YearQuarterYearWeek 类型上,如下所示

PastEvent.java
public static class PastEvent {

    @Past
    private YearWeek yearWeek;
    @Past
    private YearQuarter yearQuarter;

    public PastEvent(YearWeek yearWeek, YearQuarter yearQuarter) {
        this.yearWeek = yearWeek;
        this.yearQuarter = yearQuarter
    }
}
FutureEvent.java
public static class FutureEvent {

    @Future
    private YearWeek yearWeek;
    @Future
    private YearQuarter yearQuarter;

    public FutureEvent(YearWeek yearWeek, YearQuarter yearQuarter) {
        this.yearWeek = yearWeek;
        this.yearQuarter = yearQuarter
    }
}

您也可以在 GitHub 上找到这个示例。

结论

所以,如您所见,可以完全独立地构建和共享自定义约束验证器。这只需几个简单步骤即可完成

  • 创建一个实现 ConstraintValidator 接口的验证器

  • META-INF/services/javax.validation.ConstraintValidator 文件中引用此验证器的全限定名

  • (可选) 通过添加 ContributorValidationMessages.properties 文件来定义自定义/默认消息

  • 将其打包成一个 JAR

  • 您现在可以分享您的约束了,人们只需将您的 JAR 添加到类路径即可使用


返回顶部