Bean Validation

本文将介绍BeanValidation的历史发展、名词解释、以及内置的一些注解、类等等。关于与Spring的整合使用请查看另外一篇文章:Spring - 数据校验。

 

参考资料:
bean Validation官网

 

一、Bean Validation简介

1、Bean Validation介绍

 
什么是Bean Validation ?

Bean Validation是一种规范, 为 JavaBean 和方法验证定义了一组元数据模型和 API 规范,常用于后端数据的声明式校验。

 
为什么要使用Bean Validation ?

验证数据是发生在所有应用程序层(从表示层到持久层)的常见任务。通常在每一层中实现相同的验证逻辑,这既耗时又容易出错。为了避免重复这些验证,开发人员经常将验证逻辑直接捆绑到域模型中,将域类与验证代码混淆,验证代码实际上是关于类本身的元数据。

Bean Validation_第1张图片
 
Bean Validation 规范定义了用于 JavaBean 验证的元数据模型和 API。默认的元数据源是注解,能够通过使用 XML 验证描述符来覆盖和扩展元数据。API 不依赖于特定的应用程序层或编程模型。它特别不依赖于 Web 或持久层,并且可用于服务器端应用程序编程以及富客户端 Swing 应用程序开发人员。

Bean Validation_第2张图片

 

2、历史发展

Bean Validation 规范最早在 Oracle Java EE 下维护。

2017 年 11 月,Oracle 将 Java EE 移交给 Eclipse 基金会。 2018 年 3 月 5 日,Eclipse 基金会宣布 Java EE (Enterprise Edition) 被更名为 Jakarta EE。

因此 Bean Validation 规范经历了下面两个阶段:
 
第一阶段:JavaEE Bean Validation

  • 2009.10.12 Bean Validation 1.0(JSR-303) - Java EE 6
  • 2013.04.10 Bean Validation 1.1(JSR-349) - Java EE 7
  • 2017.06.21 Bean Validation 2.0.0.CR1(JSR-380)- Java EE 8

第二阶段:Jakarta Bean Validation

  • 2019.08 Jakarta Bean Validation 2.0 - Jakarta EE 8
  • 2020.10.7 akarta Bean Validation 3.0 - Jakarta EE 9

注意:Jakarta Bean Validation 2.0 和 Bean Validation 2.0之间没有变化,就是改了一下包名,变为:jakarta.validation:jakarta.validation-api.

 

3、版本特性

1、Bean Validation1.1

依赖注入

Bean验证使用了一些组件MessageInterpolatorTraversableResolverParameterNameProviderConstraintValidatorFactoryConstraintValidator

Bean Validation 1.1 标准化了容器如何管理这些对象以及这些对象如何从容器服务中变得有用。

 
方法验证

Bean Validation 1.1 允许对任意方法和构造函数的参数和返回值设置约束:

  • 调用者在调用方法或构造方法之前必须满足的先决条件。
  • 调用者在方法或构造方法调用返回后所保证的后置条件。

 
分组转换:(ConvertGroup)

执行级联验证时,可以通过使用组转换功能转换到另外一个组。组转换是通过使用@ConvertGroup注释声明的。

 
通过统一表达语言进行消息插值

约束冲突消息 (即错误消息) 现在可以使用EL表达式来实现更灵活的呈现和字符串格式化。

特别是在EL上下文中注入格式化器对象,将数字、日期等转换为特定于语言环境的字符串表示形式。

同样,经过验证的值在EL上下文中也是可用的。

 
 

2、Bean Validation 2.0

Bean Validation 2.0 的主要贡献是利用 Java 8 的新语言特性和 API 添加来进行验证。使用 Bean Validation 2.0 需要 Java 8 或更高版本。

变化包括:

  • 支持通过注释参数化类型的类型参数来验证容器元素

    • 更灵活的集合类型级联验证;例如*,*现在可以验证映射的Key和Value:Map<@Valid CustomerType, @Valid Customer> customersByType
    • 支持 java.util.Optional
    • 通过插入附加值提取器来支持自定义容器类型(请参阅值提取器定义)
  • 全新的内置约束限制:@Email@NotEmpty@NotBlank@Positive@PositiveOrZero@Negative@NegativeOrZero@PastOrPresent@FutureOrPresent(内置约束定义)

  • 所有内置约束现在都标记为可重复

  • 使用反射检索参数名称(参阅命名参数)

  • ConstraintValidator#initialize()是默认方法(参阅约束验证实现)

  • Bean Validation XML 描述符的命名空间与约束映射文件已更改为http://xmlns.jcp.org/xml/ns/validation/configurationforMETA-INF/validation.xmlhttp://xmlns.jcp.org/xml/ns/validation/mapping(请参阅XML 配置:META-INF/validation.xml)

 
 

4、Hibernate Validator 与 Bean Validation的关系 ?

Bean Validation只是一个规范,而Hibernate Validator为Bean Validation的具体实现,目前有三个稳定版本:7.0、 6.2、6.0 。请查看:http://hibernate.org/validator/releases/

Hibernate Validator 7.0 6.2 6.0
Java 8, 11 or 17 8, 11 or 17 8, 11 or 17
Bean Validation N/A N/A 2.0
Jakarta Bean Validation 3.0 2.0 N/A

以下介绍Bean Validation规范中的一些重要概念与Api。
 
 

二、Constraints介绍

Constraints 翻译过来就是约束的意思,用于定义校验的规则,比如:@Null、@NotNull等。

Constraints 是 Bean Validation 规范的核心,通过约束注解和一系列约束验证实现的组合来定义的。

约束注解可应用于类型、字段、方法、构造函数、参数、容器元素或其它约束注解。

 

1、Bean Validation 内建Constraints

在Bean Validation 2.0中定义了22个内建的约束校验注解,如下:
内建约束注解

1.1 空与非空检查

注解 支持Java类型 说明
@Null Object 为null
@NotNull Object 不为null
@NotBlank CharSequence 不为null,且必须有一个非空格字符
@NotEmpty CharSequence、Collection、Map、Array 不为null,且不为空(length/size>0)

 
1.2 Boolean值检查

注解 支持Java类型 说明 备注
@AssertTrue boolean、Boolean 为true 为null有效
@AssertFalse boolean、Boolean 为false 为null有效

 

1.3 日期检查

注解 支持Java类型 说明 备注
@Future Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间之后 为null有效
@FutureOrPresent Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间或之后 为null有效
@Past Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间之前 为null有效
@PastOrPresent Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间或之前 为null有效

 
1.4 数值检查

注解 支持Java类型 说明 备注
@Max BigDecimal、BigInteger,byte、short、int、long以及包装类 小于或等于 为null有效
@Min BigDecimal、BigInteger,byte、short、int、long以及包装类 大于或等于 为null有效
@DecimalMax BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 小于或等于 为null有效
@DecimalMin BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 大于或等于 为null有效
@Negative BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 负数 为null有效,0无效
@NegativeOrZero BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 负数或零 为null有效
@Positive BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 正数 为null有效,0无效
@PositiveOrZero BigDecimal、BigInteger,byte、short、int、long、float、double以及包装类 正数或零 为null有效
@Digits(integer = 3, fraction = 2) BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包装类 整数位数和小数位数上限 为null有效

 
1.5 其他

注解 支持Java类型 说明 备注
@Pattern CharSequence 匹配指定的正则表达式 为null有效
@Email CharSequence 邮箱地址 为null有效,默认正则 '.*'
@Size CharSequence、Collection、Map、Array 大小范围(length/size>0) 为null有效

 

2、hibernate-validator 内建的Constraints

在hibernate-validator 扩展了Constraints,参考:Additional constraints

以下是部分示例:

注解 支持Java类型 说明
@Length String 字符串长度范围
@Range 数值类型和String 指定范围
@URL URL地址验证

 

3、第三方的 Constraints

Java Bean Validation Extension

Collection Validators

 
 

三、Constraints 定义

通过上面的介绍我们知道Constraint是由**约束注解以及约束的验证实现**组合定义的。

在组合定义的情况下,约束注释应用于类型、字段、方法、构造函数、参数、容器元素或其他约束注释。

除非另有说明,否则 Jakarta Bean 验证 API 的默认包名称是javax.validation

这涉及到@Constraint约束注解与ConstraintValidator类,下面则主要介绍的是@Constraint约束注解的属性、定义;ConstraintValidator的使用等方面。

 

1、约束注解介绍

一个比较完整的示例可能如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

    String message() default "电话错误";

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

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

这里面包含了以下信息:

  • 该校验注解保留策略包含RUNTIME

  • 使用javax.validation.Constraint进行修饰指定Bean Validation 校验器的实现。

  • 定义了一组属性message、groups、payload。(其实还漏了一个,下面介绍)。

  • @Target针对FIELD有效。

 
以下介绍相关的属性

1.1 作用类型范围限定 - @Target

这一点由@Target注解来完成,该注解属于Java标准注解内的注解,其限定属性在ElementType中定义,如下:

/** Class, interface (including annotation type), or enum declaration */
TYPE,

/** Field declaration (includes enum constants) */
FIELD,

/** Method declaration */
METHOD,

/** Formal parameter declaration */
PARAMETER,

/** Constructor declaration */
CONSTRUCTOR,

/** Local variable declaration */
LOCAL_VARIABLE,

/** Annotation type declaration */
ANNOTATION_TYPE,

/** Package declaration */
PACKAGE,

/**
 * Type parameter declaration
 *
 * @since 1.8
 */
TYPE_PARAMETER,

/**
 * Use of a type
 *
 * @since 1.8
 */
TYPE_USE

 
Jakarta Bean Validation约束在大多数情况下要么是通用约束,要么是跨参数约束。在少数情况下,约束可能是两者兼有。

通用约束注释可以针对以下任何一个ElementTypes:

  • FIELD 对于受约束的属性
  • METHOD 对于受约束的 getter 和受约束的方法返回值
  • CONSTRUCTOR 构造器
  • PARAMETER 对于受约束的方法和构造函数参数
  • TYPE 对于受限bean
  • ANNOTATION_TYPE 对于构成其他约束的约束
  • TYPE_USE 对于容器元素约束

 
跨参数约束注释可以针对以下任何一个ElementTypes:

  • METHOD
  • CONSTRUCTOR
  • ANNOTATION_TYPE 用于约束跨参数的,它构成了其他跨参数约束

 

1.2 约束注解属性

其实在约束注解中定义任何属性都是可以的,但有四个是保留属性,分别是:message、groups、validationAppliesTo 和 payload

如果想定义其他约束注解属性,不允许以valid开头

 

1.2.1 message

标识错误消息,在定义每个约束注解所必须要定义的,也可以称之为消息描述符,是一个字符串文字,可能包含一个或多个消息参数或表达式。

 

1.2.1.1 消息插值

什么是消息参数与消息表达式 ?

这与消息插值有关,从Bean Validation1.1开始就支持统一表达语言进行消息插值。将消息的描述符转换为完全扩展的、人类可读的错误消息。

而消息参数与消息表达式正是所支持的两种插值方式:

  • 消息参数 - 包含在{}中的字符串文字
  • 消息表达式 - 包含在${}中的字符串文字

 

在使用消息描述符的过程中涉及到到诸如:‘{’、‘}’的字符的如何处理 ?

对于转义字符的处理,以下规则适用:

  • \{被视为文字{而不是被视为消息参数的开头
  • \}被视为文字}而不是被视为消息参数的结尾
  • \\被视为文字\而不是被视为转义字符
  • \$被视为文字$而不是被视为消息表达式的开始

 
消息插值的先决条件 ?

  • 每个Constraint通过其message属性定义一个消息描述符。
  • 每个Constraint定义都为该约束定义了一个默认消息描述符。
  • 通过在Constraint上设置message属性,可以在Constraint声明时覆盖消息。

 
原理 ?

消息参数与消息表达式的解析过程是由MessageInterpolator来完成的,它必须遵守定义的算法来内插消息描述符,默认的消息内插器使用以下步骤:

  1. 从消息字符串中提取消息参数,并用作ResourceBundle``ValidationMessages``/ValidationMessages.properties使用定义的语言环境搜索命名(通常具体化为属性文件及其语言环境变体)的关键字。如果找到属性,则消息参数将替换为消息字符串中的属性值。【比如:@xxxx(message=“{contunryName}”)、contunryName=中国】。

    递归地应用步骤 1,直到没有执行替换(即消息参数值本身可以包含消息参数)。【比如:contunryName={country}, country=中国】

  2. 从消息字符串中提取消息参数,并作为key, 使用已定义的地区(参见地区了解默认消息插值)搜索Jakarta Bean Validation提供程序的内置ResourceBundle。如果找到属性,则用消息字符串中的属性值替换消息参数。

    与步骤1相反,步骤2不是递归处理的。

  3. 如果步骤2触发替换,则再次应用步骤1。否则执行步骤4。

  4. 从消息字符串中提取消息参数。在Constraint声明中,那些与Constraint的属性名称匹配的属性将被该属性的值替换。

    比如:min、max会被替换成0、100

    @Size(min = 0,max = 100,message = "大小必须在{min} - {max之间}")
    private int size;
    

    参数插值优先于消息表达式。例如,对于消息描述符 v a l u e ,尝试将 v a l u e 作为消息参数计算优先于将 {value},尝试将{value}作为消息参数计算优先于将 value,尝试将value作为消息参数计算优先于将{value}作为消息表达式计算。

  5. 从消息字符串中提取消息表达式,并使用Jakarta表达式语言进行计算。

 
消息参数:

{}符号,形式上表现为如下:

@Size(min = 0,max = 100,message = "大小必须在{min} - {max之间}")
private int size;

取值范围:

  • ResourceBundle``ValidationMessages``/ValidationMessages.properties内的值
  • 内置的ResourceBundle内的值
  • Constraint的属性名对应的值

 
消息表达式:

${},形式上表现为如下:

@Size(min = 0,max = 100,message = "大小必须不满足,当前值为${validatedValue}")
private int size;

消息表达式通过Jakarta表达式语言进行解析计算,Jakarta表达式语言遵循以下规则:

  • Constraint的属性名对应的值。

  • 在名称validatedValue下映射的验证值。

  • 一个 叫formatter 的bean 映射到名称为format(String format, Object... args)可变形参方法。

    此方法必须表现得像java.util.Formatter.format(String format, Object... args). 用于格式化的区域设置由用于默认消息插值的区域设置定义。formatter bean允许格式化属性值,

    例如:在验证值是98.12345678的情况下,${formatter.format('%1$.2f', validatedValue)}将其格式化为98.12(小数点后两位数字)。

如果在消息插值期间发生异常,例如由于无效的表达式或对未知属性的引用,则消息表达式保持不变。

 

1.2.1.2 国际化

强烈建议将消息值默认为资源束键,以启用国际化。还建议使用以下约定:Resource Bundle中设置的 key 连接到message的约束注释的完全限定类名,比如:

String message() default "{com.acme.constraint.MyConstraint.message}";

----------Resource Bundle 文件内的内容-----------
com.acme.constraint.MyConstraint.message=[自定义消息]

步骤:

1、定义国际化文件,且文件名称必须为ValidationMessages。

2、定义message属性为消息参数(即 "{IsMobile.message}"这样的格式),并在国际化文件中添加这个属性。

Bean Validation_第3张图片

 

/**
 * 用于验证手机号是否是以134开头的
 */
@Target({TYPE, FIELD, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {


    String message() default "{IsMobile.message}";

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

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

}

 

1.2.2 group

每个约束注解都必须定义一个groups元素,该元素指定与约束声明相关联的处理组,在分组校验中会用到。 参数的类型groupsClass[]

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

默认值必须是一个空数组。

如果在对元素声明约束时未指定则认为该组为Default组。

groups通常用于控制计算约束的顺序,或者执行JavaBean部分状态的验证。

 

1.2.3 payload

约束注释必须定义一个payload 元素,该元素指定约束声明与之关联的有效负载(类似于log中的error、info等级别,用于表示其严重程度,框架可以利用这种严重性来调整约束失败的显示方式)。payload参数的类型为payload[]。

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

默认值必须是一个空数组。

每个附加的payload 必须实现Payload接口。
 

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

public class Address {
    @NotNull(message="would be nice if we had one", payload=Severity.Info.class)
    public String getZipCode() { [...] }

    @NotNull(message="the city is mandatory", payload=Severity.Error.class)
    String getCity() { [...] }
}

有效负载信息可以通过ConstraintDescriptor从错误报告中检索,可以通过ConstraintViolation对象(参见ConstraintViolation)或通过metadata API(参见 ConstraintDescriptor)访问。

 

1.2.4 validationAppliesTo (约束目标)

在Constraint声明时使用validationAppliesTo来澄清约束的目标(例如,带注释的元素、方法返回值或方法参数),如下:

ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;

 
使用场景:

常见的场景是当一个Constraint注解标注在方法上时,由于该方法既有返回值又有参数,在这种情况下就需要澄清约束目标,是约束返回值呢? 还是参数呢?

在这种场景下validationAppliesTo是必须的。如果违反了这些规则,将引发ConstraintDefinitionException。

validationAppliesTo参数的类型是Constraintarget,默认值必须是constraintarget . implicit。

public enum ConstraintTarget {

    /*没有歧义下的类型发现:
     *如果既不是在方法上,也不是在构造函数上,则表示注释的元素 (type, field等),
	 *如果在一个没有参数的方法或构造函数上,它意味着 {@code RETURN_VALUE} 
	 *如果一个方法没有返回值({@code void}),它意味着 {@code PARAMETERS}
	 *否则,{@code IMPLICIT}不被接受,{@code RETURN_VALUE}或{@code PARAMETERS}是必需的。这种情况适用于带参数的构造函数和带参数和返回值的方法
     */
    IMPLICIT,

    /**
     * 约束应用于方法或构造函数的返回值
     */
    RETURN_VALUE,

    /**
     * 约束应用于方法或构造函数的参数
     */
    PARAMETERS
}

如果在非法情况下使用了constraintarget,则在验证时或请求元数据时将引发ConstraintDeclarationException。非法情况的例子有:

  • 在无法推断的情况下使用IMPLICIT,
  • 在没有参数的构造函数或方法上使用PARAMETERS,
  • 在没有返回值的方法上使用RETURN_VALUE,
  • 在类型(类或接口)或字段上使用PARAMETERS或RETURN_VALUE。
  • 当使用同时支持方法或构造函数的约束时,鼓励约束用户显式设置constraintarget目标,因为它提高了可读性。

 

1.3 示例

简单约束定义

// 将String标记为表示格式良好的订单号
@Documented
@Constraint(validatedBy = OrderNumberValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface OrderNumber {

    String message() default "{com.acme.constraint.OrderNumber.message}";

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

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

 
交叉参数约束定义

// 交叉参数约束,确保方法的两个日期参数按正确的顺序排列。
@Documented
@Constraint(validatedBy = DateParametersConsistentValidator.class)
@Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface DateParametersConsistent {

    String message() default "{com.acme.constraint.DateParametersConsistent.message}";

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

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

 
即是泛型又是交叉参数的约束定义

// 使用el表达式进行验证,该约束接受任何类型,并且可以验证带注释的类型或跨参数应用限制。
@Documented
@Constraint(validatedBy = ELAssertValidator.class)
@Target({ METHOD, FIELD, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ELAssert {

    String message() default "{com.acme.constraint.ELAssert.message}";

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

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

    ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;

    String expression();
}

@ELAssert(
    message="Please check that your passwords match and try again.",
    expression="param[1]==param[2]",
    validationAppliesTo=ConstraintType.PARAMETERS
)
public User createUser(String email, String password, String repeatPassword) { [...] }

泛型和跨参数的约束显示了既可以应用于带注释的元素又可以应用于方法或构造函数的跨参数的约束。注意,在本例中存在validationAppliesTo。

 
带默认参数的约束定义

@Documented
@Constraint(validatedBy = AudibleValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface Audible {

    Age age() default Age.YOUNG;

    String message() default "{com.acme.constraint.Audible.message}";

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

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

    public enum Age {
        YOUNG,
        WONDERING,
        OLD
    }
}

确保给定的频率在人耳的范围内。约束定义包括在应用约束时可以指定的可选参数。

 
带强制参数的约束定义

@Documented
@Constraint(validatedBy = DiscreteListOfIntegerValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface Acceptable {

    int[] value();

    String message() default "{com.acme.constraint.Acceptable.message}";

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

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

定义了一个表示为数组的可接受值列表:value应用约束时必须指定该属性。

 

1.3 可重复约束注解定义

对目标的同一属性多次使用相同的约束是很常见的,比如由于存在组的概念,因此某个属性上可能会存在多个@Pattern,用于判断是否与该正则表达式匹配。

为了支持这一需求,Jakarta Bean Validation提供程序以一种特殊的方式处理其值元素具有约束注释数组返回类型的常规注释(没有由@Constraint注释的注释)。值数组中的每个元素都由Jakarta Bean Validation作为常规约束注释来处理实现。这意味着value元素中指定的每个约束都应用于目标。注释必须具有保留RUNTIME,可以应用于type, field,property, executable parameter, executable return value, executable cross-parameter or another annotation(类型、字段、属性、可执行参数、可执行返回值、可执行跨参数或其他注释)。建议使用与初始约束相同的目标集

约束设计人员注意:每个约束注释都应该与其相应的多值注释相耦合。该规范建议(虽然没有强制)定义名为List的内部注释。每个约束注释类型都应该用java.lang.annotation进行元注释。可重复,引用相应的List注释。这将约束注释类型标记为可重复的,并允许用户多次指定约束,而无需显式使用List注释。所有内置注释都遵循这个模式。

 
示例:

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

    String countryCode();

    String message() default "{com.acme.constraint.ZipCode.message}";

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

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

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        ZipCode[] value();
    }
}


public class Address {
    @ZipCode(countryCode = "fr", groups = Default.class, message = "zip code is not valid")
    @ZipCode(countryCode = "fr",groups = SuperUser.class,message = "zip code invalid. Requires overriding before saving.")
    private String zipCode;
}

 
在此示例中,两个约束都适用于该zipCode字段,但具有不同的组和不同的错误消息。还可以通过显式使用@List注释多次指定约束(尽管简单地重复注释是 Jakarta Bean Validation 2.0 和 Java 8 的首选习惯用法)

public class Address {
    @ZipCode.List( {
        @ZipCode(countryCode="fr", groups=Default.class,message = "zip code is not valid"),
        @ZipCode(countryCode="fr", groups=SuperUser.class,message = "zip code invalid. Requires overriding before saving.")
    } )
    private String zipCode;
}

 

1.4 组合约束注解定义

很多情况下对于某个属性可能包含多个约束注解,此时我们可以采取将这多个约束组合起来作为一个约束注解。

优点:

  • 避免重复并促进对更基本的约束的重用。
  • 将原语约束作为元数据API中组合约束的一部分公开,并增强工具意识。

组合是通过使用组合约束注解 / 注解 / 约束注解来完成的。
 

@Pattern(regexp = "[0-9]*")
@Size(min = 5, max = 5)
@Constraint(validatedBy = FrenchZipCodeValidator.class)
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {

    String message() default "Wrong zip code";

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

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

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FrenchZipCode[] value();
    }
}

说明:

  • 用@FrenchZipCode(组合注释)注释一个元素相当于用@Pattern(regexp=“[0-9]*”)、@Size(min=5, max=5)(组合注释)和@FrenchZipCode注释它。
  • 约束注释上的每个约束注释都应用于目标元素,这是递归完成的。
  • 默认情况下,每个失败约束生成一个错误报告。
  • 组合注解继承了主约束注解中的gorous,组合注解上的任何gorous定义都将被忽略。
  • 组合注解继承了主约束注解的Payload。组合注解上的任何Payload定义都将被忽略。
  • 组合注释继承主约束注解中validationAppliesTo(约束目标)。对组合注解的任何validationAppliesTo定义都会被忽略。
  • 放置组合约束的类型必须与所有约束(组合和组合)兼容。约束设计人员应该确保这样的类型存在,并在JavaDoc中列出所有兼容的类型。
  • 所有组合约束和组合约束都必须具有共同的约束类型。特别是,将纯泛型约束和纯交叉参数约束混合在一起是不合法的。

 

1.4.1 @ReportAsSingleViolation

如果任何组合注解失败,托管此注解的约束注解将返回组合注解错误报告,每个单独的组合约束的错误报告将被忽略。

在此场景中,如果一个或多个组合注解无效,则会自动认为主要约束无效,并生成相应的错误报告。

如果想要任何一个组合约束失败,则会引发@FrenchZipCode对应的错误报告,而不会引发其他错误,请使用@ReportAsSingleViolation注释。

@Pattern(regexp = "[0-9]*")
@Size(min = 5, max = 5)
@ReportAsSingleViolation
@Constraint(validatedBy = FrenchZipCodeValidator.class)
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {

    String message() default "Wrong zip code";

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

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

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FrenchZipCode[] value();
    }
}

更具体地说,如果组合约束标记为@ReportAsSingleViolation,则组合约束的评估在第一个失败的约束处停止,并且生成并返回与组合约束相对应的错误报告。

 

1.4.2 @OverridesAttribute

观察上述组合注解发现,组合注解可以定义消息的值和自定义属性(不包括组、有效负载和validationAppliesTo),但对于像@Size的min、max、@Pattern的regexp等是写死的,对于每个使用@FrenchZipCode注解的属性等而言应该是可以变化的。

通过使用@OverridesAttribute,在主注解中定义的属性可以用于覆组合注解的一个或多个属性。需要注意的是覆盖属性与被覆盖属性的类型必须相同。
 

@Pattern(regexp = "[0-9]*")
@Size
@Constraint(validatedBy = FrenchZipCodeValidator.class)
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {

    String message() default "Wrong zip code";

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

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

    @OverridesAttribute(constraint = Size.class, name = "min")
    @OverridesAttribute(constraint = Size.class, name = "max")
    int size() default 5;

    @OverridesAttribute(constraint = Size.class, name = "message")
    String sizeMessage() default "{com.acme.constraint.FrenchZipCode.zipCode.size}";

    @OverridesAttribute(constraint = Pattern.class, name = "message")
    String numberMessage() default "{com.acme.constraint.FrenchZipCode.number.size}";

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        FrenchZipCode[] value();
    }
}

使用@OverridesAttribute (@FrenchZipCode.sizeMessage)注释的组合约束属性的值被应用到以@OverridesAttribute.name命名的组合约束属性,并驻留在类型为@OverridesAttribute的组合约束(@Size.message)上。

同样,@FrenchZipCode.numberMessage值被映射到@Pattern.message。

如果未定义name属性,则@OverridesAttribute.name的默认值是包含@OverridesAttribute注释的组合约束属性的名称(比如:sizeMessage、numberMessage)。
 
特别Note:

组合约束本身可以是组合约束。在这种情况下,根据描述的规则递归地覆盖属性值。但是请注意,转发规则(由@OverridesAttribute定义)只应用于直接组合约束。

 

1.4.2.1 constraintIndex

对于组合约束可能会出现多个重复约束的情况,如果组合约束是直接在组合约束上给出的(即通过可重复注释特性),则constraintIndex指的是这种类型的约束从左到右的顺序,这些约束是在组合约束上给出的。

如果使用对应的List注释指定组合约束,则constraintIndex指向值数组中的索引(比如:@ZipCode.List(@ZipCode(。。。),@ZipCode(。。。))。·
 

Documented
@Constraint(validatedBy = {})
@Pattern(regexp = "[A-Z0-9._%+-][email protected][A-Z0-9.-]+\\.[A-Z]{2,4}") // email
@Pattern(regexp = ".*?emmanuel.*?") // emmanuel
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface EmmanuelsEmail {

    String message() default "Not emmanuel's email";

    @OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 0)
    String emailMessage() default "Not an email";

    @OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 1)
    String emmanuelMessage() default "Not Emmanuel";

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

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

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        EmmanuelsEmail[] value();
    }
}

 

1.4.2.2 OverridesAttribute 总结

以下元素唯一标识一个被覆盖的约束属性:

  • @OverridesAttribute.constraint
  • @OverridesAttribute.name
  • @OverridesAttribute.constraintIndex

如果组合无效,例如

  • 无限递归组合
  • 错误的属性覆盖
  • 一个属性映射到多个源属性
  • 标记为不同约束类型(即泛型和跨参数)的组合约束和组合约束
  • 等等。

则会在验证时或在请求元数据时引发ConstraintDefinitionException
 
鼓励约束设计者根据规范定义的内置约束使用组合(递归或不递归)。组合约束通过 Jakarta Bean Validation 元数据 API ( ConstraintDescriptor )公开。此元数据对于第三方元数据使用者特别有用,例如生成数据库模式的持久性框架(例如 Jakarta Persistence)或表示框架。

 
 

2、约束验证器介绍

约束验证器主要用于对给定的约束注解与类型进行验证,约束验证器由约束注解定义的@Constraint注释的validatedBy元素指定,并且约束验证器必须实现ConstraintValidator接口

 

2.1 ConstraintValidator - 约束验证器

2.1.1 ConstraintValidator

/*
 * 定义为给定对象类型T验证给定约束A的逻辑。
 * 实现必须遵守以下限制:
 *    T必须解析为非参数化类型(即因为该类型未使用泛型或因为使用原始类型而不是泛型版本)
 *    或T泛型参数必须是无界通配符类型
 */
public interface ConstraintValidator<A extends Annotation, T> {

    /*
     * 初始化验证器以准备isValid(Object, ConstraintValidatorContext)调用。 传递给定约束声明的约束注释。
     * 保证在使用此实例进行验证之前调用此方法。
     * 默认实现是无操作。
     */
    default void initialize(A constraintAnnotation) {
    }

    /*
     * 实现验证逻辑。 value状态不得改变。
	 * 该方法可以并发访问,必须通过实现来保证线程安全。
	 * true为通过验证、false为未通过验证
	 * value 要验证的参数, context 约束校验上下文
	 */
    boolean isValid(T value, ConstraintValidatorContext context);
}

如果在initialize()或isValid()方法中发生异常,则运行时异常被Jakarta Bean验证引擎包装到ValidationException中。

约束验证实现不允许更改传递给isValid()的值的状态。

 
注意:

对于null值的情况,是返回为true还是false需要进行考虑。

 

2.1.2 @SupportedValidationTarget

用于定义ConstraintValidator可以验证的目标。
ConstraintValidator可以针对由约束注释的(返回的)元素、方法或构造函数(又名交叉参数)的参数数组或两者。

如果@SupportedValidationTarget不存在,则ConstraintValidator以ConstraintValidator注释的(返回的)元素为目标。
以交叉参数为目标的ConstraintValidator必须接受Object[] (或Object )作为它验证的对象类型。
 

@Documented
@Target({ TYPE })
@Retention(RUNTIME)
public @interface SupportedValidationTarget {
	ValidationTarget[] value();
}

public enum ValidationTarget {

	/**
	 * (返回)由约束注释的元素。
	 */
	ANNOTATED_ELEMENT,

	/**
	 * 带注释的方法或构造函数的参数数组(又名交叉参数)。
	 */
	PARAMETERS
}

 
示例:

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ScriptAssertValidator implements ConstraintValidator<ScriptAssert,Object[]> {
    @Override
    public void initialize(ScriptAssert constraintAnnotation) {
        [...]
    }

    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        [...]
    }
}

 

2.1.3 示例

实现必须遵守以下限制:

  • T必须解析为非参数化类型(即因为该类型未使用泛型或因为使用原始类型而不是泛型版本)

  • 或T泛型参数必须是无界通配符类型

有效的ConstraintValidator:

//String is not making use of generics
public class SizeValidatorForString implements ConstraintValidator<Size, String> {
    [...]
}

//Collection uses generics but the raw type is used
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection> {
    [...]
}

//Collection uses generics and unbounded wildcard type
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection<?>> {
    [...]
}

//Validator for cross-parameter constraint
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class DateParametersConsistentValidator
    implements ConstraintValidator<DateParametersConsistent, Object[]> {
    [...]
}

//Validator for both annotated elements and executable parameters
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS})
public class ELScriptValidator implements ConstraintValidator<ELScript, Object> {
    [...]
}

 
无效的ConstraintValidator:

//parameterized type (参数化类型)
public class SizeValidatorForString implements ConstraintValidator<Size, Collection<String>> {
    [...]
}

//parameterized type using bounded wildcard (使用有界通配符的参数化类型)
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection<? extends Address>> {
    [...]
}

//cross-parameter validator accepting the wrong type (跨参数的验证接受了错误的类型,应该是Object对象或Object数组而不是Number)
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class NumberPositiveValidator implements ConstraintValidator<NumberPositive, Number> {
    [...]
}

 

2.2 ConstraintValidatorContext - 约束校验上下文

2.2.1 介绍

传递给isValid()方法的ConstraintValidatorContext对象携带约束被验证到的上下文中可用的信息和操作。
 

public interface ConstraintValidatorContext {

	/**
	 * 禁用默认的ConstraintViolation对象生成(使用在约束上声明的消息模板)。
	 * 用于设置不同的违规消息或基于不同的属性生成ConstraintViolation 。
	 */
	void disableDefaultConstraintViolation();

	/**
	 * 返回:当前未插值的默认消息
	 */
	String getDefaultConstraintMessageTemplate();

	/**
	 * 以Clock的形式返回用于获取当前时间的提供者,例如在验证Future和Past约束时。
	 * 返回:
     * 获取当前时间的提供者,从不为null 。 
     * 如果在引导期间没有配置特定的提供程序,则将返回使用当前系统时间和Clock.systemDefaultZone()返回的当前默认时区      		 * 的默认实现
	 *
	 * @since 2.0
	 */
	ClockProvider getClockProvider();

	/**
	 * 返回构建违规报告的约束违规生成器,允许有选择地将其关联到子路径。 违规消息将被插入。 可以看一下这里类上的注释
	 * @param messageTemplate 新的未插值约束消息
	 * @return returns 约束违规生成器
	 */
	ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);

	/**
	 * 返回指定类型的实例,允许访问提供者特定的 API。 如果 Bean 验证提供程序实现不支持指定的类,则抛出ValidationException
	 *
	 * @since 1.1
	 */
	<T> T unwrap(Class<T> type);
    
    [。。。]
}

ConstraintValidatorContext接口提供了对特定约束验证有用的上下文信息的访问(例如,getlockprovider())。

它还允许重新定义当约束无效时生成的默认约束消息(即你定义约束注解时定义的mesage属性值)。

默认情况下,每个无效的约束将导致生成一个错误对象,该错误对象由ConstraintViolation违例对象表示。

此对象由约束声明定义的默认约束消息模板和放置约束声明的上下文(bean、属性、可执行参数、跨参数、可执行返回值或容器元素)构建。

ConstraintValidatorContext方法让约束实现禁用默认的constraint违例生成(调用disableDefaultConstraintViolation),并创建一个或多个自定义生成。作为参数传递的非插值消息用于构建ConstraintViolation消息(消息插值操作应用于它)。

默认情况下,constraint违例中公开的Path表示bean、属性、参数、跨参数、返回值或承载约束的容器元素的路径(有关更多信息,请参阅constraint违例)。您可以使用约束违背构建器fluent API将其指向此默认路径的子路径。

 

2.2.2 示例

1、ConstraintValidator 简单实现

/**
 * Check that a String begins with one of the given prefixes.
 */
public class BeginsWithValidator implements ConstraintValidator<BeginsWith, String> {

    private Set<String> allowedPrefixes;

    /**
     * Configure the constraint validator based on the elements specified at the time it was
     * defined.
     *
     * @param constraint the constraint definition
     */
    @Override
    public void initialize(BeginsWith constraint) {
        allowedPrefixes = Arrays.stream( constraint.value() )
                .collect( collectingAndThen( toSet(), Collections::unmodifiableSet ) );
    }

    /**
     * Validate a specified value. returns false if the specified value does not conform to
     * the definition.
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null )
            return true;

        return allowedPrefixes.stream()
                .anyMatch( value::startsWith );
    }
}

 
2、交叉参数 ConstraintValidator 实现

/**
 * 检查方法的两个日期参数是否按预期顺序排列。预期已验证方法的第2和第3个参数为java.util.Date类型。
 */
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class DateParametersConsistentValidator implements
        ConstraintValidator<DateParametersConsistent, Object[]> {

    /**
     * Validate a specified value. returns false if the specified value does not conform to
     * the definition
     */
    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        if ( value.length != 3 ) {
            throw new IllegalArgumentException( "Unexpected method signature" );
        }
        // one or both limits are unbounded => always consistent
        if ( value[1] == null || value[2] == null ) {
            return true;
        }
        return ( (Date) value[1] ).before( (Date) value[2] );
    }
}

 
3 、泛型和跨参数ConstraintValidator 实现

/**
 * 检查对象是否通过约束提供的Jakarta Expression Language表达式。
 */
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS})
public class ELScriptValidator implements ConstraintValidator<ELScript, Object> {

    public void initialize(ELScript constraint) {
        [...]
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        [...]
    }
}    

 

2.2.3 重新定义约束消息

需要做的是,首先通过disableDefaultConstraintViolation()方法关闭默认ConstraintValidator,并将重新定义的约束消息作为buildConstraintViolationWithTemplate()的参数再调用addConstraintViolation方法即可重新定义约束消息。

更加复杂的操作可以参考:重新定义约束消息
 

/**
 * 检查String类型的元素是否以"SN-"开头以及是否满足合适的长度
 * 错误的消息使用以下的其中一个:
 * 	if 元素不以"SN-"开头,则使用:{com.acme.constraint.SerialNumber.wrongprefix}
 * 	if 元素不满足特定长度,则使用:{com.acme.constraint.SerialNumber.wronglength}
 * 
 */
public class SerialNumberValidator implements ConstraintValidator<SerialNumber, String> {

    private int length;

    /**
     * Configure the constraint validator based on the elements specified at the time it was
     * defined.
     *
     * @param constraint the constraint definition
     */
    @Override
    public void initialize(SerialNumber constraint) {
        this.length = constraint.length();
    }

    /**
     * Validate a specified value. returns false if the specified value does not conform to
     * the definition.
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null )
            return true;

        context.disableDefaultConstraintViolation();

        if ( !value.startsWith( "SN-" ) ) {
            String wrongPrefix = "{com.acme.constraint.SerialNumber.wrongprefix}";
            context.buildConstraintViolationWithTemplate( wrongPrefix )
                    .addConstraintViolation();
            return false;
        }
        if ( value.length() != length ) {
            String wrongLength = "{com.acme.constraint.SerialNumber.wronglength}";
            context.buildConstraintViolationWithTemplate( wrongLength )
                    .addConstraintViolation();
            return false;
        }
        return true;
    }
}

 

2.2.4 时间约束校验器

时间约束的约束验证器(无论是内置的约束@Past, @PastOrPresent, @Future和@FutureOrPresent或自定义的时间约束)可以从ConstraintValidatorContext# getlockprovider()暴露的ClockProvider对象中获取当前瞬间,ClockProvider接口如下:

public interface ClockProvider {

    /**
     * getClock()方法返回一个java.time.Clock对象,该对象使用时区表示当前的即时、日期和时间
     */
    Clock getClock();
}

 
示例:

// 是否是过去时间
public class PastValidatorForZonedDateTime implements ConstraintValidator<Past, ZonedDateTime> {

    @Override
    public boolean isValid(ZonedDateTime value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }

        ZonedDateTime now = ZonedDateTime.now( context.getClockProvider().getClock() );

        return value.isBefore( now );
    }
}

 
 

四、约束验证

该节主要介绍约束验证的要求、目标对象等。
 

1、约束验证对象的要求

Jakarta Bean进行验证时,对验证的Bean有以下要求:

  • 要验证的属性必须遵循JavaBeans 规范中定义读取属性的方法签名约定即getter。

    JavaBeans 规范指定 getter 是一种方法
    
    getxxx - 具有返回类型但没有参数
    
    isxxx - 返回boolean值但没有参数
    
  • 静态字段和静态方法被排除在验证之外。

  • 约束可以应用于接口(interface)和超类(SuperClass),但同样也存在限制接下来会说明。

 

2、约束验证的目标

约束验证的目标可以是以下:

  • type
  • field or property 字段或属性
  • constructor or method return value 方法的返回值
  • constructor or method parameter 构造器或方法的参数
  • constructor or method cross-parameter 构造器或方法的交叉参数
  • container element 容器元素

前提是:

  • 约束定义支持指定的目标 ( java.lang.annotation.Target)
  • 约束上声明的一个ConstraintValidator支持声明的目标类型,或者在跨参数的情况下,存在一个跨参数的ConstraintValidator(参阅ConstraintValidator解析算法以了解ConstraintValidator解析)
  • 在容器元素约束的情况下,存在相应的值提取器(有关值提取器解析的详细信息,请参阅ValueExtractor解析)

 

2.1 field or property 字段或属性

约束声明可以应用于同一对象类型的字段和属性。 但是,不应在字段及其关联属性之间重复相同的约束(约束验证将应用两次),遵循单一状态策略。

作用于字段:

@NotNull
private String name;

作用于属性:需要满足JavaBeans规范,即存在无参的getxx()方法(对于boolean也可以是isxxx()方法)

@NotNull
public String getName(){
    return name;
}

 

2.2 container element 容器元素

约束可以应用于通用容器的元素,例如ListMapOptional

这是通过将约束注解添加到此类容器的类型参数来完成的。

容器元素约束可以在以下声明中使用:

  • 字段 field
  • 属性 properties
  • 方法或构造方法的参数
  • 方法的返回值
private List<@Email String> emails;

public Optional<@Email String> getEmail() {
    [...]
}

public Map<@NotNull String, @ValidAddress Address> getAddressesByType() {
    [...]
}

public List<@NotBlank String> getMatchingRecords(List<@NotNull @Size(max=20) String> searchTerms) {
    [...]
}

 

2.2.1 规则

1、

容器元素约束可以应用于嵌套容器类型:

private Map<String, @NotEmpty List<@ValidAddress Address>> addressesByType;

 
2、

不支持在泛型类型或方法的类型参数上声明容器元素约束。

也不支持在类型定义的extendsorimplements子句中声明对类型参数的容器元素约束:

public class NonNullList<@NotNull T> {
    [...]
}

public class ContainerFactory {
    <@NotNull T> Container<T> instantiateContainer(T wrapped) { [...] }
}

public class NonNullSet<T> extends Set<@NotNull T> {
    [...]
}

 

2.2.2 容器的隐式解包

使用场景:

除了在类型参数 (即<>) 上指定容器元素约束之外,还可以在非通用容器类型上声明容器元素约束。

这是通过隐式解包完成的,即约束不适用于带注释的容器本身,而是应用于其元素。

 
比如:

java.util.OptionalIntOptionalLongOptionalDouble

以及JavaFX的的非通用的属性类型,例如StringPropertyIntegerProperty

@Min(1)
private OptionalInt optionalNumber;

@Negative
private LongProperty negativeLong;

@Positive
private IntegerProperty positiveInt;

private final ListProperty<@NotBlank StringProperty> notBlankStrings;

以上注解都不是作用于对象本身,而是作用于被包装的数字和字符串值。

 
原理:

你必须要定义一个明确可解析的值提取器(请参阅将容器级约束应用于容器元素的 ValueExtractor 解析算法),它带有@UnwrapByDefault注释(请参阅@UnwrapByDefault)

如果需要,可以通过UnwrapSkip有效负载定义明确指定在容器上声明的约束的目标(容器或容器元素);

两者不可同时定义,否则抛出ConstraintDeclarationException异常:
 

public interface Unwrapping {

    /**
     * 在验证之前展开值。
     *
     * @since 2.0
     */
    public interface Unwrap extends Payload {
    }

    /**
     * 如果通过{@link UnwrapByDefault}注释在{@link ValueExtractor}上启用了解包装,则跳过它。  
     *
     * @since 2.0
     */
    public interface Skip extends Payload {
    }

}

 
例如:

@NotNull约束应用于StringProperty容器:

@NotNull(payload = Unwrapping.Skip.class)
private StringProperty name;

@Email 作用于List容器中的元素

@Email(payload = Unwrapping.Unwrap.class)
List emails;

等价于
List<@Email String> emails; //直接标注在类型参数上更好

 

2.3 constructor or method 参数与返回值

特点:

  • 验证会忽略static 静态方法,对静态方法施加约束是不可移植的。

  • 对BeanValidation2.0来说,对方法不存在其他限制,但是与方法验证相结合的技术可能会对应用验证的方法施加进一步的限制。例如,某些集成技术可能要求要验证的方法必须具有public可见性和/或不能final

  • 为了对方法或构造函数参数使用约束注解,它们的元素类型必须是ElementType.PARAMETER。

  • 为了在跨参数验证或方法或构造函数的返回值上使用约束注解,它们的元素类型必须是ElementType.METHOD、ElementType.CONSTRUCTOR。 所有内置约束都支持这些元素类型,对于自定义约束,最好也这样做。

  • 这些约束在方法调用时不会自行验证,而是需要集成层调用验证

 

2.3.1 方法、构造器参数验证

参数约束:

通过在方法或构造函数参数上放置约束注解(下例中的NotNull)来声明。

public class OrderService {

    public OrderService(@NotNull CreditCardProcessor creditCardProcessor) {
        [...]
    }

    public void placeOrder(
        @NotNull @Size(min=3, max=20) String customerCode,
        @NotNull Item item,
        @Min(1) int quantity) {
        [...]
    }
}

 
交叉参数约束:

通过在方法或构造函数上放置交叉参数约束注解(下例中的@ConsistentDateParameters)来声明。

public class CalendarService {

    @ConsistentDateParameters
    public void createEvent(
        String title,
        @NotNull Date startDate,
        @NotNull Date endDate) {
        [...]
    }
}

 
注意:

一些约束注解可以针对可执行文件的返回值及其参数数组,此时如果存在歧义就需要约束注解上标注validationAppliesTo的属性值为ConstraintTarget.PARAMETERS。

(上面的那个并不会发生歧义,因为该方法返回值为void,根据ConstraintTarget.IMPLICIT它选择的就是作用目标就是参数数组) 。

 

2.3.2 方法返回值验证

返回值约束是通过将约束注解直接放在方法或构造器上来声明的。

同样的类似于方法|构造器参数验证,需要在约束注解上标注validationAppliesTo为ConstraintTarget.RETURN_VALUE。

public class OrderService {

    private CreditCardProcessor creditCardProcessor;

    @ValidOnlineOrderService
    public OrderService(OnlineCreditCardProcessor creditCardProcessor) {
        this.creditCardProcessor = creditCardProcessor;
    }

    @ValidBatchOrderService
    public OrderService(BatchCreditCardProcessor creditCardProcessor) {
        this.creditCardProcessor = creditCardProcessor;
    }

    @NotNull
    @Size(min=1)
    public Set getCreditCardProcessors() { [...] }

    @NotNull
    @Future
    public Date getNextAvailableDeliveryDate() { [...] }
}

这里定义了以下后置条件,保证了OrderService类的方法和构造器的调用遵循以下方面:

  • 第一个构造函数返回的新创建的OrderService对象满足自定义@ValidOnlineOrderService约束的条件。

  • 第二个构造函数返回的新创建的OrderService对象满足自定义@ValidBatchOrderService约束的条件。

  • 由getCreditCardProcessors()返回的CreditCardProcessor对象集既不会为空也不会为空。

  • getNextAvailableDeliveryDate()返回的Date对象不会为空,而是在将来。

 

2.3.3 继承层次中的方法约束

在继承层次结构中定义方法约束(即通过扩展基类的类继承和通过实现或扩展接口的接口继承)时,必须遵守Liskov 替换原则,该原则要求:

  • 不能在子类型中加强方法的前提条件(由参数约束表示)
  • 不能在子类型中削弱方法的后置条件(由返回值约束表示)

仅适用于一般方法,验证构造函数约束时不适用,因为构造函数不会相互重写。

 
非法示例:

1、非法声明的参数约束:

public interface OrderService {

    void placeOrder(String customerCode, Item item, int quantity);
}

public class SimpleOrderService implements OrderService {

    @Override
    public void placeOrder(
        @NotNull @Size(min=3, max=20) String customerCode,
        @NotNull Item item,
        @Min(1) int quantity) {
        [...]
    }
}

约束SimpleOrderService是非法的,因为它们加强了placeOrder()由接口构成的方法的先决条件OrderService

 

2、对并行类型非法声明的参数约束

public interface OrderService {

    void placeOrder(String customerCode, Item item, int quantity);
}

public interface OrderPlacementService {

    public void placeOrder(
        @NotNull @Size(min=3, max=20) String customerCode,
        @NotNull Item item,
        @Min(1) int quantity);
}

public class SimpleOrderService implements OrderService, OrderPlacementService {

    @Override
    public void placeOrder(String customerCode, Item item, int quantity) {
        [...]
    }
}

在这里,类SimpleOrderService实现了两个接口OrderService和OrderPlacementService,这两个接口彼此不相关,但都定义了一个具有相同签名的方法placeOrder()。

这个层次结构对于参数约束是不合法的,因为SimpleOrderService的客户端必须满足在OrderPlacementService接口上定义的约束,即使客户端只期望OrderService。

 
正确示例:

1、正确声明子类的返回值约束

public class OrderService {

    Order placeOrder(String customerCode, Item item, int quantity) {
        [...]
    }
}

public class SimpleOrderService extends OrderService {

    @Override
    @NotNull
    @Valid
    public Order placeOrder(String customerCode, Item item, int quantity) {
        [...]
    }
}

 
2、正确的子类参数约束

public interface OrderService {

    void placeOrder(@NotNull @Size(min=3, max=20) String customerCode,
        @NotNull Item item,
        @Min(1) int quantity);
}

public class SimpleOrderService implements OrderService {

    @Override
    public void placeOrder(
        String customerCode,
        Item item,
        int quantity) {
        [...]
    }
}

 

3、级联验证

Jakarta Bean Validation API 不仅允许验证单个类实例,还允许验证完整的对象图(级联验证)。

可以通过@Valid来标记,用于验证级联的属性、方法参数或方法返回类型。

当验证属性、方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。此行为以递归方式应用。

在级联验证期间会忽略null值。

 

3.1 作用目标

3.1.1 field or property 字段与属性

public class Car {

    @NotNull
    @Valid
    private Person driver;

}
public class Person {

    @NotNull
    private String name;

}

 

3.1.2 container element 容器元素

集合值、数组值以及通常的Iterable容器本身以及类型参数,包括:

  • 对象数组
  • java.util.Collection
  • java.util.Set
  • java.util.List
  • java.util.Map

特点:

  • 迭代器提供的每个对象都经过验证。

  • 对于Map,每个的值(由 检索getValueMap.Entry被验证( 注意:Key 未被验证)

  • Jakarta Bean Validation 2.0 开始 @Valid还允许验证嵌套的通用容器的元素。

  • @Valid必须放入该嵌套容器类型的类型参数(即 <>内),以触发对所有嵌套容器的元素的验证。

  • @Valid注释应该放在容器本身容器的类型参数上,但不能同时放在两者上(以防止容器元素被验证两次)。

  • 不支持放入@Valid泛型类型或方法的类型参数;也不支持@Valid在类型定义的extendsorimplements子句中放入类型参数。

    比如下面的错误示例:

    public class NonNullList<@Valid T> {
        [...]
    }
    
    public class ContainerFactory {
        <@Valid T> Container<T> instantiateContainer(T wrapped) { [...] }
    }
    
    public class NonNullSet<T> extends Set<@Valid T> {
        [...]
    }
    

 
示例:

1、List

public class User {

    // Jakarta Bean Validation 2.0 首选风格
    private List<@Valid PhoneNumber> phoneNumbers;

    // 传统风格
    @Valid
    private List<PhoneNumber> phoneNumbers;

    // 禁止,在容器本身与类型参数上放置@Valid注解
    @Valid
    private List<@Valid PhoneNumber> phoneNumbers;
}

 
2、map

public class User {

    // Jakarta Bean Validation 2.0 首选风格
    private Map<AddressType, @Valid Address> addressesByType;

    // 传统风格
    @Valid
    private Map<AddressType, Address> addressesByType;

    // 禁止,映射或映射值类型参数都应该用@Valid注释,但不能两者都用
    @Valid
    private Map<AddressType, @Valid Address> addressesByType;
}

 
3、map的key与vaue进行级联验证

public class User {

    private Map<@Valid AddressType, @Valid Address> addressesByType;
}

 
4、嵌套列表元素的map进行级联验证

public class User {
    private Map<String, List<@Valid Address>> addressesByType;
}

 
5、嵌套map元素的map进行级联验证

public class User {
    private Map<String, Map<@Valid AddressType, @Valid Address>> addressesByUserAndType;
}

 

3.1.3 constructor or method 参数与返回值

@Valid注解也可用于方法 / 构造器参数返回值执行级联验证。

标记时,参数或返回值被认为是要验证的bean对象。

同样的这些约束在方法调用时不会自行验证,而是需要集成层调用验证

 
使用:

  • 方法 | 构造器参数级联验证:直接在方法参数上添加@Valid即可。
  • 方法 | 构造器返回值级联验证:直接在方法上添加@Valid即可。
public class OrderService {

    @NotNull @Valid
    private CreditCardProcessor creditCardProcessor;

    @Valid
    public OrderService(@NotNull @Valid CreditCardProcessor creditCardProcessor) {
        this.creditCardProcessor = creditCardProcessor;
    }

    @NotNull @Valid
    public Order getOrderByPk(@NotNull @Valid OrderPK orderPk) { [...] }

    @NotNull
    public Set<@Valid Order> getOrdersByCustomer(@NotNull @Valid CustomerPK customerPk) { [...] }
}

下面的递归验证将在验证OrderService类的方法时发生:

  • 对传递给构造函数的creditCardProcessor参数的对象的约束进行验证
  • 对构造函数返回的新创建的OrderService实例的约束进行验证,即字段creditCardProcessor上的@NotNull约束和引用的creditCardProcessor实例上的约束(因为字段是用@Valid注释的)。
  • 对传递给orderPk参数的对象和getOrderByPk()方法返回的Order对象的约束进行验证
  • 验证传递给customerPk参数的对象上的约束,以及getOrdersByCustomer()方法返回的Set中包含的每个对象上的约束

 

4、分组验证

使用场景 ?

有时我们需要在不同情况下才会对某个属性进行不同的验证校验顺序的控制,这时我们就可以采取分组验证。

 

4.1 分组验证

如何分组?

每一个约束注解都包含groups属性,可以通过指定groups属性来进行分组;

如果没有明确声明组,则约束属于该Default组。


public interface Billable {
    
}

public interface BuyInOneClick {
}

public class User {
    @NotNull
    private String firstname;

    @NotNull(groups = Default.class)
    private String lastname;

    @NotNull(groups = {Billable.class, BuyInOneClick.class})
    private CreditCard defaultCreditCard;
}

 
分析:

  • 在验证调用期间,将验证一个或多个组。

  • 在对象图上评估属于这组的所有约束。

  • 在将组分配给约束中,当Billable或BuyInOneClick组被验证时,将在defaultCreditCard上检查@NotNull。

  • 在验证Default组时,验证firstname和lastname上的@NotNull。提醒:父类和接口上的约束被考虑。

 
如何进行分组验证 ?

方式一:Validator的validate(T object, Class… groups)方法。

public class Driver {
  @Min(value = 18, groups = Minimal.class)
  int age;

  @AssertTrue
  Boolean passedDrivingTest;

  @Valid
  Car car;

  // setter/getters
}

public class Car {
  @NotNull
  String type;

  @AssertTrue(groups = Later.class)
  Boolean roadWorthy;

  // setter/getters
}


Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Driver driver = new Driver();
driver.setAge(16);
Car porsche = new Car();
driver.setCar(porsche);
Set<ConstraintViolation<Driver>> violations = validator.validate( driver, Minimal.class );

 
方式二:如果与Spring整合的话,使用Spring提供的@Validated注解,可以指定要验证的组

@PostMapping("/beanValid")
public String beanValid(@RequestBody @Validated({Minimal.class} Driver bean){
     return "success";
}
           

 

4.2 组继承

在某些情况下,一个组是一个或多个组的超集,一个组可以通过接口继承来继承一个或多个组,比如下面这样:

public interface BuyInOneClick extends Default, Billable {
    
}

 
组继承验证规则:

public class User {
    @NotNull
    private String firstname;

    @NotNull(groups = Default.class)
    private String lastname;

    @NotNull(groups = Billable.class)
    private CreditCard defaultCreditCard;
}

在验证组BuyInOneClick将导致以下约束检查:

  • @NotNullfirstnamelastname
  • @NotNulldefaultCreditCard

因为DefaultBillable是 的超接口BuyInOneClick,属于BuyInOneClick的一部分。

 

问题 ?

那是否可以设置一个子类组以及对应的父类组呢 ?

 

4.2 组序

默认情况下,无论约束属于哪个组,都不会按特定顺序评估约束(比如你指定了验证组序列为:Default、Billable,但并不一定会按照这个顺序)。

然而,在某些情况下控制约束评估的顺序很有用,比如:

  • 第二组依赖于稳定状态才能正常运行。这种稳定状态由第一组验证。
  • 第二组是时间、CPU 或内存的大量消耗者,应尽可能避免对其进行评估。

 
Bean Validation使用@GroupSequence来定义组序列,从左往右排序(需要特别注意的是,当标注在class类上时有所不同)。

@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface GroupSequence {

    Class<?>[] value();
}

 
规则:

1、组序列中存在多个组(例如:@GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class})),会按照顺序一组一组的评估约束,只有当一组约束评估有效时才会评估下一组约束。

2、定义序列的组和组成序列的组不得参与循环依赖:

  • 直接或间接
  • 通过级联序列定义或组继承

3、如果对包含此类循环的组求值,则会引发GroupDefinitionException异常。

4、定义序列的组不应该直接继承其他组,换句话说,承载组序列的接口不应该有任何超级接口,比如下面的错误示例:

 @GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class})
 public interface  PhoneSequence extends OttherInterface{
     
 }

5、定义序列的组不应该在约束声明中直接使用。换句话说,承载组序列的接口不应该在约束声明中使用,比如下面的错误示例。

 @GroupSequence(value = {IsMobileGroups.HUNAN.class,Default.class})
 public interface  PhoneSequence{ 
     
 }
public class User {
    @NotNull(group={PhoneSequence.class})
    private String firstname;
	。。。
}

 

正确示例:

@ZipCodeCoherenceChecker(groups = Address.HighLevelCoherence.class)
public class Address {
    @NotNull @Size(max = 50)
    private String street1;

    @NotNull @ZipCode
    private String zipCode;

    @NotNull @Size(max = 30)
    private String city;
    
    --------------------------------------------
    public interface HighLevelCoherence {}

    @GroupSequence({Default.class, HighLevelCoherence.class})
    public interface Complete {}
}

Address.Complete组被验证时,属于该Default组的所有约束都被验证。

如果其中任何一个失败,验证将跳过该HighLevelCoherence组。

如果所有Default约束都通过,HighLevelCoherence则会被约束验证。

 

4.3 重定义默认组

什么是重定义默认组 ?

实际上有点像 “重新指向”,当我们以Default组验证时,以Default组开始,按照标注在类上的@GroupSequence内指定的顺序来进行校验;一旦中间某个组校验过程出现校验不通过,后续组将不再校验。

 
规则:

  • 可以通过在类上标注@GroupSequence注解来重定义默认组,但这个默认组就是所标注类AA.class,且之前的Default.class不得在出现在该组序列(@GroupSequence)中声明。

  • 托管在类A并属于Default组(默认或显式)的约束注解的groups属性都隐式属于组A

@GroupSequence({Address.class, HighLevelCoherence.class})
@ZipCodeCoherenceChecker(groups = Address.HighLevelCoherence.class)
public class Address {
    @NotNull @Size(max = 50)
    private String street1;

    @NotNull @ZipCode
    private String zipCode;

    @NotNull @Size(max = 30)
    private String city;
    
    public interface HighLevelCoherence {}
}

在为Address重新定义默认组中,当为Default组验证Address对象时,将评估属于Default组并承载在Address上的所有约束。

如果没有失败,将评估Address上的所有HighLevelCoherence约束。换句话说,当为Address验证Default组时,将使用在Address类上定义的组序列。

 
比如下面这样:

@Data
@GroupSequence(value = {CovertGroupBean.class, CovertGroup.class, SimpleGroup.class})
public class CovertGroupBean {

    @NotEmpty(groups = Default.class,  message = "defaultValue不能呢为空")
    private String defaultValue;

    @NotEmpty(groups = CovertGroup.class, message = "covertValue不能呢为空")
    private String covertValue;

    @NotEmpty(groups = SimpleGroup.class,  message = "simpleValue不能呢为空")
    private String simpleValue;
}

 

@PostMapping("/defaultCovertValid")
public String defaultCovertValid(@Validated(value = Default.class) @RequestBody CovertGroupBean bean){

    return "success";
}

以Default组进行校验,

如果Default组满足校验规则,则会校验CovertGroup组;

如果CovertGroup满足规则,则会校验SimpleGroup。

如果SimpleGroup满足规则,那么整个bean的校验流程就结束啦。

一旦中间某个校验不满足,则会终止后面的校验。

 

4.4 组转换

bean Validation 2.0新特性之一,在执行级联验证时,可以使用与最初使用组转换特性请求的组不同的组。

组转换是通过使用@ConvertGroup注释声明的。

@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
public @interface ConvertGroup {

	Class<?> from() default Default.class;


	Class<?> to();

	@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	public @interface List {

		ConvertGroup[] value();
	}
}

规则:

  • @ConvertGroup需要搭配@Valid使用,则会引发一个ConstraintDeclarationException。
  • 当一个元素被@Valid注释时,验证将被传播。除非使用@ConvertGroup注释,否则组将原样传递给嵌套的元素。
  • 如果期望传递给嵌套元素验证的组被定义为@ConvertGroup注释的from属性,那么用于有效验证嵌套元素的组就是定义在to属性中的对应组。
  • 如果未指定from属性的值,则Default.class将被用作转换的源组。
  • 规则不会递归执行。如果找到匹配的规则,则不再计算后续规则。特别是,如果一组@ConvertGroup声明链A到B, B到C,则A组将被转换为B,而不是C。这将使规则更清晰。
  • 有多个转换规则包含相同的from值是不合法的。在这种情况下,将引发一个ConstraintDeclarationException。
  • 与常规约束声明一样,from属性不能引用组序列。在这种情况下会引发ConstraintDeclarationException。to属性可以。然后在验证关联对象之前展开组序列。
  • 当存在继承层次的方法约束时,同样,如果子类重写了在两个不同接口或者一个类和一个接口中一摸一样的方法,则不能为该方法的返回值声明组转换规则。这也是为了避免向调用者保证的后置条件的意外更改。(请查看 示例三)

 
示例:

简单示例:

public interface Complete extends Default {}
public interface BasicPostal {}
public interface FullPostal extends BasicPostal {}

public class Address {
    @NotNull(groups=BasicPostal.class)
    String street1;

    String street2;

    @ZipCode(groups=BasicPostal.class)
    String zipCode;

    @CodeChecker(groups=FullPostal.class)
    String doorCode;
}

public class User {
    @Valid
    @ConvertGroup(from=Default.class, to=BasicPostal.class)
    @ConvertGroup(from=Complete.class, to=FullPostal.class)
    Set<Address> getAddresses() { [...] }
}

User使用Default组验证实例时,关联的地址将通过BasicPostal组进行验证。

User使用Complete组验证实例时,关联的地址将通过FullPostal组进行验证。

 
容器元素验证的组转换:

public class User {
    Set<
        @Valid
        @ConvertGroup(from=Default.class, to=BasicPostal.class)
        @ConvertGroup(from=Complete.class, to=FullPostal.class)
        Address
    > getAddresses() { [...] }
}

 
非法组转换:

public interface BasicPostal {}

public class Order { [...] }

public interface RetailOrderService {

    @Valid
    Order placeOrder(String itemNo, int quantity);
}

public interface B2BOrderService {

    @Valid
    @ConvertGroup(from=Default.class, to=BasicPostal.class)
    Order placeOrder(String itemNo, int quantity);
}

public class OrderService implements RetailOrderService, B2BOrderService {

    @Override
    public Order placeOrder(String itemNo, int quantity) {
        [...]
    }
}

 

@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
public @interface ConvertGroup {

	Class<?> from() default Default.class;


	Class<?> to();

	@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	public @interface List {

		ConvertGroup[] value();
	}
}

 
规则:

  • @ConvertGroup需要搭配@Valid使用,则会引发一个ConstraintDeclarationException。
  • 当一个元素被@Valid注释时,验证将被传播。除非使用@ConvertGroup注释,否则组将原样传递给嵌套的元素。
  • 如果期望传递给嵌套元素验证的组被定义为@ConvertGroup注释的from属性,那么用于有效验证嵌套元素的组就是定义在to属性中的对应组。
  • 如果未指定from属性的值,则Default.class将被用作转换的源组。
  • 规则不会递归执行。如果找到匹配的规则,则不再计算后续规则。特别是,如果一组@ConvertGroup声明链A到B, B到C,则A组将被转换为B,而不是C。这将使规则更清晰。
  • 有多个转换规则包含相同的from值是不合法的。在这种情况下,将引发一个ConstraintDeclarationException。
  • 与常规约束声明一样,from属性不能引用组序列。在这种情况下会引发ConstraintDeclarationException。to属性可以。然后在验证关联对象之前展开组序列。
  • 当存在继承层次的方法约束时,同样,如果子类重写了在两个不同接口或者一个类和一个接口中一摸一样的方法,则不能为该方法的返回值声明组转换规则。这也是为了避免向调用者保证的后置条件的意外更改。(请查看 示例三)

你可能感兴趣的:(数据校验,java,后端)