今天我们将讨论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
。
我们新的约束注解可能看起来是这样的
@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()
- 执行实际验证
一个实现可能如下所示
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
),而消息是当验证失败时你想显示的消息。我们的属性文件如下所示
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
的文件,并将验证器实现的完全限定名放入其中
com.acme.validation.validators.DurationMinValidator
完成所有这些后,你的新约束注解可以在Duration
元素上使用,如下所示
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;
}
}
项目结构应类似于以下内容

这里展示的整个源代码可以在GitHub上的hibernate-demos存储库中找到。
使用标准约束对非标准类进行验证
现在让我们考虑你想要一个标准的Bean Validation约束支持一些其他类型的情况,而不仅仅是已经支持的类型。
ThreeTen Extra类型验证
由于我们讨论了与日期/时间相关的验证,所以让我们在这个示例中也保持相同的主题。在本节中,我们将查看ThreeTen Extra类型 - 一个提供额外日期和时间类的优秀库,以补充Java中已存在的那些。
Bean Validation通过@Past
/@Future
注解提供了对时间类型的验证支持。因此,我们希望也在ThreeTen Extra类型上使用这些注解。为了使这个例子简单,我们将只为YearWeek
和YearQuarter
提供验证器。
让我们从实现ConstraintValidator
接口开始
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
文件中提供实现的验证器列表
com.acme.validation.validators.FutureYearQuarterValidator
com.acme.validation.validators.FutureYearWeekValidator
com.acme.validation.validators.PastYearQuarterValidator
com.acme.validation.validators.PastYearWeekValidator
之后,我们可以将其打包成一个 JAR 文件,并准备好使用我们的验证器与全世界分享它们!
最终,我们的项目结构应该看起来像这样

现在您可以将 @Past
和 @Future
注解放置在 YearQuarter
和 YearWeek
类型上,如下所示
public static class PastEvent {
@Past
private YearWeek yearWeek;
@Past
private YearQuarter yearQuarter;
public PastEvent(YearWeek yearWeek, YearQuarter yearQuarter) {
this.yearWeek = yearWeek;
this.yearQuarter = yearQuarter
}
}
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 上找到这个示例。