Hibernate Validator 中文操作指南
6. Creating custom constraints
Jakarta Bean 验证 API 定义了一整套标准约束注释,例如@NotNull、@Size等。在这些内置约束不充分的情况下,可以轻松地创建针对特定验证要求定制的自定义约束。
The Jakarta Bean Validation API defines a whole set of standard constraint annotations such as @NotNull, @Size etc. In cases where these built-in constraints are not sufficient, you can easily create custom constraints tailored to your specific validation requirements.
6.1. Creating a simple constraint
要创建自定义约束,需要以下三个步骤:
To create a custom constraint, the following three steps are required:
-
Create a constraint annotation
-
Implement a validator
-
Define a default error message
6.1.1. The constraint annotation
本部分展示如何编写约束注解,该注解可用于确保给定字符串完全为大写或小写。随后,此约束将应用到 Chapter 1, Getting started 中 Car 类的 licensePlate 字段,以确保该字段始终为大写字符串。
This section shows how to write a constraint annotation which can be used to ensure that a given string is either completely upper case or lower case. Later on, this constraint will be applied to the licensePlate field of the Car class from Chapter 1, Getting started to ensure that the field is always an upper-case string.
首先需要一种表达两种大小写模式的方法。虽然可以使用String常量,但更好的方法是为此目的使用枚举:
The first thing needed is a way to express the two case modes. While you could use String constants, a better approach is using an enum for that purpose:
. Example 6.1: Enum CaseMode to express upper vs. lower case
package org.hibernate.validator.referenceguide.chapter06;
public enum CaseMode {
UPPER,
LOWER;
}
下一步是定义实际的约束注释。如果您以前从未设计过注释,这可能看起来有点可怕,但实际上并不难:
The next step is to define the actual constraint annotation. If you’ve never designed an annotation before, this may look a bit scary, but actually it’s not that hard:
. Example 6.2: Defining the @CheckCase constraint annotation
package org.hibernate.validator.referenceguide.chapter06;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
@Repeatable(List.class)
public @interface CheckCase {
String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase." +
"message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
CaseMode value();
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
@interface List {
CheckCase[] value();
}
}
使用@interface关键字定义注释类型。注释类型的所有属性都以类似于方法的方式声明。Jakarta Bean 验证 API 的规范要求任何约束注释定义:
An annotation type is defined using the @interface keyword. All attributes of an annotation type are declared in a method-like manner. The specification of the Jakarta Bean Validation API demands, that any constraint annotation defines:
-
an attribute message that returns the default key for creating error messages in case the constraint is violated
-
an attribute groups that allows the specification of validation groups, to which this constraint belongs (see Chapter 5, Grouping constraints). This must default to an empty array of type Class<?>.
-
an attribute payload that can be used by clients of the Jakarta Bean Validation API to assign custom payload objects to a constraint. This attribute is not used by the API itself. An example for a custom payload could be the definition of a severity:
_public class Severity { public interface Info extends Payload { }
_public class Severity { public interface Info extends Payload { }
public interface 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;
// ... }_Now a client can after the validation of a _ContactDetails_ instance access the severity of a constraint using _ConstraintViolation.getConstraintDescriptor().getPayload()_ and adjust its behavior depending on the severity.
除了这三个必填属性外,还有另一个属性value,允许指定所需的大小写模式。名称value是一个特殊的名称,如果它是指定的唯一属性,则可以在使用注释时省略,例如在@CheckCase(CaseMode.UPPER)中。
Besides these three mandatory attributes there is another one, value, allowing for the required case mode to be specified. The name value is a special one, which can be omitted when using the annotation, if it is the only attribute specified, as e.g. in @CheckCase(CaseMode.UPPER).
此外,约束注释还装饰有一些元注释:
In addition, the constraint annotation is decorated with a couple of meta annotations:
-
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}): Defines the supported target element types for the constraint. @CheckCase may be used on fields (element type FIELD), JavaBeans properties as well as method return values (METHOD), method/constructor parameters (PARAMETER) and type argument of parameterized types (TYPE_USE). The element type ANNOTATION_TYPE allows for the creation of composed constraints (see Section 6.4, “Constraint composition”) based on @CheckCase.
创建类级约束时(参见 Section 2.1.4, “Class-level constraints”),必须使用元素类型 TYPE。针对构造函数返回值的约束需要支持元素类型 CONSTRUCTOR。用于一起验证方法或构造函数的所有参数的跨参数约束(参见 Section 6.3, “Cross-parameter constraints”),必须分别支持 METHOD 或 CONSTRUCTOR。
When creating a class-level constraint (see Section 2.1.4, “Class-level constraints”), the element type TYPE would have to be used. Constraints targeting the return value of a constructor need to support the element type CONSTRUCTOR. Cross-parameter constraints (see Section 6.3, “Cross-parameter constraints”) which are used to validate all the parameters of a method or constructor together, must support METHOD or CONSTRUCTOR, respectively.
-
@Retention(RUNTIME): Specifies, that annotations of this type will be available at runtime by the means of reflection
-
@Constraint(validatedBy = CheckCaseValidator.class): Marks the annotation type as constraint annotation and specifies the validator to be used to validate elements annotated with @CheckCase. If a constraint may be used on several data types, several validators may be specified, one for each data type.
-
@Documented: Says, that the use of @CheckCase will be contained in the JavaDoc of elements annotated with it
-
@Repeatable(List.class): Indicates that the annotation can be repeated several times at the same place, usually with a different configuration. List is the containing annotation type.
此名为List的包含注释类型也在示例中显示。它允许在同一元素上指定多个@CheckCase注释,例如具有不同的验证组和消息。虽然可以使用其他名称,但 Jakarta Bean 验证规范建议使用名称List,并使注释成为相应约束类型的内部注释。
This containing annotation type named List is also shown in the example. It allows to specify several @CheckCase annotations on the same element, e.g. with different validation groups and messages. While another name could be used, the Jakarta Bean Validation specification recommends to use the name List and make the annotation an inner annotation of the corresponding constraint type.
6.1.2. The constraint validator
定义注释后,您需要创建能够使用@CheckCase注释验证元素的约束验证程序。为此,请按如下所示实现 Jakarta Bean 验证接口ConstraintValidator:
Having defined the annotation, you need to create a constraint validator, which is able to validate elements with a @CheckCase annotation. To do so, implement the Jakarta Bean Validation interface ConstraintValidator as shown below:
. Example 6.3: Implementing a constraint validator for the constraint @CheckCase
package org.hibernate.validator.referenceguide.chapter06;
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
@Override
public void initialize(CheckCase constraintAnnotation) {
this.caseMode = constraintAnnotation.value();
}
@Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if ( object == null ) {
return true;
}
if ( caseMode == CaseMode.UPPER ) {
return object.equals( object.toUpperCase() );
}
else {
return object.equals( object.toLowerCase() );
}
}
}
ConstraintValidator 接口定义两个类型参数,它们在实现中设置。第一个指定要验证的注释类型 (CheckCase),第二个指定验证器可以处理的元素类型 (String)。如果约束支持多种数据类型,则必须针对每个允许的类型实现一个 ConstraintValidator,并按上述方式在约束注释处进行注册。
The ConstraintValidator interface defines two type parameters which are set in the implementation. The first one specifies the annotation type to be validated (CheckCase), the second one the type of elements, which the validator can handle (String). In case a constraint supports several data types, a ConstraintValidator for each allowed type has to be implemented and registered at the constraint annotation as shown above.
验证器的实现很简单。initialize() 方法使您可以访问已验证约束的属性值,并允许您将它们存储在验证器的字段中,如示例所示。
The implementation of the validator is straightforward. The initialize() method gives you access to the attribute values of the validated constraint and allows you to store them in a field of the validator as shown in the example.
isValid() 方法包含实际的验证逻辑。对于 @CheckCase,这是在取决于 initialize() 中检索的大小写模式的情况下,检查给定字符串是完全小写还是完全大写的。请注意,Jakarta Bean Validation 规范建议将 null 值视为有效值。如果 null 不是元素的有效值,则应明确用 @NotNull 对其进行注释。
The isValid() method contains the actual validation logic. For @CheckCase this is the check whether a given string is either completely lower case or upper case, depending on the case mode retrieved in initialize(). Note that the Jakarta Bean Validation specification recommends to consider null values as being valid. If null is not a valid value for an element, it should be annotated with @NotNull explicitly.
6.1.2.1. The ConstraintValidatorContext
Example 6.3, “Implementing a constraint validator for the constraint @CheckCase” 仅从 isValid() 方法返回 true 或 false ,依靠默认的错误消息生成。使用传递的 ConstraintValidatorContext 对象,可以添加其他错误消息或完全禁用默认错误消息生成,而仅定义自定义错误消息。 ConstraintValidatorContext API 被建模为流式接口,最好通过一个示例加以说明:
Example 6.3, “Implementing a constraint validator for the constraint @CheckCase” relies on the default error message generation by just returning true or false from the isValid() method. Using the passed ConstraintValidatorContext object, it is possible to either add additional error messages or completely disable the default error message generation and solely define custom error messages. The ConstraintValidatorContext API is modeled as fluent interface and is best demonstrated with an example:
. Example 6.4: Using ConstraintValidatorContext to define custom error messages
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorcontext;
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
@Override
public void initialize(CheckCase constraintAnnotation) {
this.caseMode = constraintAnnotation.value();
}
@Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if ( object == null ) {
return true;
}
boolean isValid;
if ( caseMode == CaseMode.UPPER ) {
isValid = object.equals( object.toUpperCase() );
}
else {
isValid = object.equals( object.toLowerCase() );
}
if ( !isValid ) {
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(
"{org.hibernate.validator.referenceguide.chapter06." +
"constraintvalidatorcontext.CheckCase.message}"
)
.addConstraintViolation();
}
return isValid;
}
}
Example 6.4, “Using ConstraintValidatorContext to define custom error messages” 说明如何禁用默认错误消息生成并使用指定的模板添加自定义错误消息。在本示例中,使用 ConstraintValidatorContext 会生成与默认错误消息生成相同错误消息。
Example 6.4, “Using ConstraintValidatorContext to define custom error messages” shows how you can disable the default error message generation and add a custom error message using a specified message template. In this example the use of the ConstraintValidatorContext results in the same error message as the default error message generation.
通过调用 addConstraintViolation() 添加每个已配置约束冲突非常重要。只有在调用后,才会创建新的约束冲突。 |
It is important to add each configured constraint violation by calling addConstraintViolation(). Only after that the new constraint violation will be created. |
默认情况下,对于 ConstraintValidatorContext 中创建的自定义违法,表达式语言未启用。
By default, Expression Language is not enabled for custom violations created in the ConstraintValidatorContext.
但是,对于某些高级要求,可能需要使用 Expression Language。
However, for some advanced requirements, using Expression Language might be necessary.
在这种情况下,您需要取消包装 HibernateConstraintValidatorContext 并明确启用表达式语言。有关详细信息,请参阅 Section 12.13.1, “HibernateConstraintValidatorContext” 。
In this case, you need to unwrap the HibernateConstraintValidatorContext and enable Expression Language explicitly. See Section 12.13.1, “HibernateConstraintValidatorContext” for more information.
请参阅 Section 6.2.1, “Custom property paths”,了解如何使用 ConstraintValidatorContext API 控制类级约束的约束违规行为的属性路径。
Refer to Section 6.2.1, “Custom property paths” to learn how to use the ConstraintValidatorContext API to control the property path of constraint violations for class-level constraints.
6.1.2.2. The HibernateConstraintValidator extension
Hibernate Validator 为 ConstraintValidator 合约提供了一个扩展:HibernateConstraintValidator。
Hibernate Validator provides an extension to the ConstraintValidator contract: HibernateConstraintValidator.
此扩展的目的是向 initialize() 方法提供更多上下文信息,因为在当前 ConstraintValidator 合约中,仅将注释作为参数传递。
The purpose of this extension is to provide more contextual information to the initialize() method as, in the current ConstraintValidator contract, only the annotation is passed as parameter.
initialize() 的 HibernateConstraintValidator 方法有两个参数:
The initialize() method of HibernateConstraintValidator takes two parameters:
-
The ConstraintDescriptor of the constraint at hand. You can get access to the annotation using ConstraintDescriptor#getAnnotation().
-
The HibernateConstraintValidatorInitializationContext which provides useful helpers and contextual information, such as the clock provider or the temporal validation tolerance.
此扩展被标记为孵化版,因此可能会发生更改。计划将来对其进行标准化并将其包含在 Jakarta Bean Validation 中。
This extension is marked as incubating so it might be subject to change. The plan is to standardize it and to include it in Jakarta Bean Validation in the future.
以下示例演示了如何基于 HibernateConstraintValidator 来验证:
The example below shows how to base your validators on HibernateConstraintValidator:
. Example 6.5: Using the HibernateConstraintValidator contract
package org.hibernate.validator.referenceguide.chapter06;
public class MyFutureValidator implements HibernateConstraintValidator<MyFuture, Instant> {
private Clock clock;
private boolean orPresent;
@Override
public void initialize(ConstraintDescriptor<MyFuture> constraintDescriptor,
HibernateConstraintValidatorInitializationContext initializationContext) {
this.orPresent = constraintDescriptor.getAnnotation().orPresent();
this.clock = initializationContext.getClockProvider().getClock();
}
@Override
public boolean isValid(Instant instant, ConstraintValidatorContext constraintContext) {
//...
return false;
}
}
你应该只实现一个 initialize() 方法。请注意,在初始化验证程序时将调用这两个方法。
You should only implement one of the initialize() methods. Be aware that both are called when initializing the validator.
6.1.2.3. Passing a payload to the constraint validator
有时,你可能希望基于一些外部参数来设定约束验证器行为。
From time to time, you might want to condition the constraint validator behavior on some external parameters.
例如,如果您为每个国家/地区有一个实例,则您的邮政编码验证器可能会根据应用程序实例的语言环境而有所不同。另一个要求可能是对特定环境有不同的行为:暂存环境可能无法访问验证器的正确功能所需的一些外部生产资源。
For instance, your zip code validator could vary depending on the locale of your application instance if you have one instance per country. Another requirement could be to have different behaviors on specific environments: the staging environment may not have access to some external production resources necessary for the correct functioning of a validator.
约束验证器负载的概念是针对所有这些用例引出的。它是通过 HibernateConstraintValidatorContext 从 Validator 实例传递给每个约束验证器的对象。
The notion of constraint validator payload was introduced for all these use cases. It is an object passed from the Validator instance to each constraint validator via the HibernateConstraintValidatorContext.
以下示例演示了如何在 ValidatorFactory 初始化期间设置约束验证器负载。除非您覆盖此默认值,否则所有 Validator_s created by this _ValidatorFactory 都会设置此约束验证器负载值。
The example below shows how to set a constraint validator payload during the ValidatorFactory initialization. Unless you override this default value, all the Validator_s created by this _ValidatorFactory will have this constraint validator payload value set.
. Example 6.6: Defining a constraint validator payload during the ValidatorFactory initialization
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.constraintValidatorPayload( "US" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
另一个选项是使用上下文为每个 Validator 设置约束验证器负载:
Another option is to set the constraint validator payload per Validator using a context:
. Example 6.7: Defining a constraint validator payload using a Validator context
HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
.configure()
.buildValidatorFactory()
.unwrap( HibernateValidatorFactory.class );
Validator validator = hibernateValidatorFactory.usingContext()
.constraintValidatorPayload( "US" )
.getValidator();
// [...] US specific validation checks
validator = hibernateValidatorFactory.usingContext()
.constraintValidatorPayload( "FR" )
.getValidator();
// [...] France specific validation checks
设置约束验证器负载后,即可在约束验证器中使用它,如下例所示:
Once you have set the constraint validator payload, it can be used in your constraint validators as shown in the example below:
. Example 6.8: Using the constraint validator payload in a constraint validator
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload;
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;
}
}
HibernateConstraintValidatorContext#getConstraintValidatorPayload() 有一个类型参数,并且仅当负载属于给定类型时才返回负载。
HibernateConstraintValidatorContext#getConstraintValidatorPayload() has a type parameter and returns the payload only if the payload is of the given type.
值得注意的是,约束验证器有效负载不同于在所提出的约束违反中可以包括的动态有效负载。 |
It is important to note that the constraint validator payload is different from the dynamic payload you can include in the constraint violation raised. |
该约束验证器有效负载的全部目的是用于限制约束验证器的行为。除非特定 ConstraintValidator 实现使用 constraint violation dynamic payload mechanism 将有效负载传递给发出的约束违反,否则不包含在约束违反中。
The whole purpose of this constraint validator payload is to be used to condition the behavior of your constraint validators. It is not included in the constraint violations, unless a specific ConstraintValidator implementation passes on the payload to emitted constraint violations by using the constraint violation dynamic payload mechanism.
6.1.3. The error message
最后缺少的一块是一个错误消息,当一个 @CheckCase 约束被违反时,应该使用这个错误消息。要定义此消息,请使用以下内容创建一个文件 ValidationMessages.properties(另请参见 Section 4.1, “Default message interpolation”):
The last missing building block is an error message which should be used in case a @CheckCase constraint is violated. To define this, create a file ValidationMessages.properties with the following contents (see also Section 4.1, “Default message interpolation”):
. Example 6.9: Defining a custom error message for the CheckCase constraint
如果发生验证错误,验证运行时会使用您为 @CheckCase 注释的 message 属性指定的默认值来查找此资源包中的错误消息。
If a validation error occurs, the validation runtime will use the default value, that you specified for the message attribute of the @CheckCase annotation to look up the error message in this resource bundle.
6.1.4. Using the constraint
现在,您可以使用 Car 章节中的 Chapter 1, Getting started 类中的约束来指定 licensePlate 仅应包含大写字符:
You can now use the constraint in the Car class from the Chapter 1, Getting started chapter to specify that the licensePlate field should only contain upper-case strings:
. Example 6.10: Applying the @CheckCase constraint
package org.hibernate.validator.referenceguide.chapter06;
public class Car {
@NotNull
private String manufacturer;
@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
private String licensePlate;
@Min(2)
private int seatCount;
public Car(String manufacturer, String licencePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
this.seatCount = seatCount;
}
//getters and setters ...
}
最后, Example 6.11, “Validating objects with the @CheckCase constraint” 示出了如何使用无效车牌验证 Car 实例致使 @CheckCase 约束违反。
Finally, Example 6.11, “Validating objects with the @CheckCase constraint” demonstrates how validating a Car instance with an invalid license plate causes the @CheckCase constraint to be violated.
. Example 6.11: Validating objects with the @CheckCase constraint
//invalid license plate
Car car = new Car( "Morris", "dd-ab-123", 4 );
Set<ConstraintViolation<Car>> constraintViolations =
validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals(
"Case mode must be UPPER.",
constraintViolations.iterator().next().getMessage()
);
//valid license plate
car = new Car( "Morris", "DD-AB-123", 4 );
constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );
6.2. Class-level constraints
如前文所述,还可以在类级别施加约束来验证整个对象的某些状态。类级别约束的定义方式与属性约束相同。 Example 6.12, “Implementing a class-level constraint”显示约束注解和您在 Example 2.9, “Class-level constraint”中已见的_@ValidPassengerCount_约束的验证器。
As discussed earlier, constraints can also be applied on the class level to validate the state of an entire object. Class-level constraints are defined in the same way as are property constraints. Example 6.12, “Implementing a class-level constraint” shows constraint annotation and validator of the @ValidPassengerCount constraint you already saw in use in Example 2.9, “Class-level constraint”.
. Example 6.12: Implementing a class-level constraint
package org.hibernate.validator.referenceguide.chapter06.classlevel;
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { ValidPassengerCountValidator.class })
@Documented
public @interface ValidPassengerCount {
String message() default "{org.hibernate.validator.referenceguide.chapter06.classlevel." +
"ValidPassengerCount.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
package org.hibernate.validator.referenceguide.chapter06.classlevel;
public class ValidPassengerCountValidator
implements ConstraintValidator<ValidPassengerCount, Car> {
@Override
public void initialize(ValidPassengerCount constraintAnnotation) {
}
@Override
public boolean isValid(Car car, ConstraintValidatorContext context) {
if ( car == null ) {
return true;
}
return car.getPassengers().size() <= car.getSeatCount();
}
}
如示例所示,您需要在 @Target 注释中使用 element type TYPE。这允许将约束放在类型定义上。示例中约束的验证器在 isValid() 方法中接收 Car,并且可以访问完整对象状态,以判定给定的实例是否有效。
As the example demonstrates, you need to use the element type TYPE in the @Target annotation. This allows the constraint to be put on type definitions. The validator of the constraint in the example receives a Car in the isValid() method and can access the complete object state to decide whether the given instance is valid or not.
6.2.1. Custom property paths
默认情况下,类级别约束的约束破坏会报告在注释类型级别,例如 Car。
By default the constraint violation for a class-level constraint is reported on the level of the annotated type, e.g. Car.
但在某些情况下,最好使破坏的属性路径引用涉及的属性之一。例如,您可能希望针对 passengers 属性而不是 Car bean 报告 @ValidPassengerCount 约束。
In some cases it is preferable though that the violation’s property path refers to one of the involved properties. For instance you might want to report the @ValidPassengerCount constraint against the passengers property instead of the Car bean.
Example 6.13, “Adding a new ConstraintViolation with custom property path” 展示了如何通过使用传递给 isValid() 的约束验证器上下文对乘客属性建立一个自定义约束违反而完成。请注意,您也可以添加若干个属性节点,指向验证 bean 的子实体。
Example 6.13, “Adding a new ConstraintViolation with custom property path” shows how this can be done by using the constraint validator context passed to isValid() to build a custom constraint violation with a property node for the property passengers. Note that you also could add several property nodes, pointing to a sub-entity of the validated bean.
. Example 6.13: Adding a new ConstraintViolation with custom property path
package org.hibernate.validator.referenceguide.chapter06.custompath;
public class ValidPassengerCountValidator
implements ConstraintValidator<ValidPassengerCount, Car> {
@Override
public void initialize(ValidPassengerCount constraintAnnotation) {
}
@Override
public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) {
if ( car == null ) {
return true;
}
boolean isValid = car.getPassengers().size() <= car.getSeatCount();
if ( !isValid ) {
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext
.buildConstraintViolationWithTemplate( "{my.custom.template}" )
.addPropertyNode( "passengers" ).addConstraintViolation();
}
return isValid;
}
}
6.3. Cross-parameter constraints
Jakarta Bean 验证区分两个不同类型的约束。
Jakarta Bean Validation distinguishes between two different kinds of constraints.
通用约束(迄今已讨论过)适用于注释元素,例如类型、字段、容器元素、方法参数或返回值等。而交叉参数约束适用于方法或构造函数的参数数组,并且可以用来说明基于多个参数值的验证逻辑。
Generic constraints (which have been discussed so far) apply to the annotated element, e.g. a type, field, container element, method parameter or return value etc. Cross-parameter constraints, in contrast, apply to the array of parameters of a method or constructor and can be used to express validation logic which depends on several parameter values.
为了定义跨参数约束,其验证器类必须使用 @SupportedValidationTarget(ValidationTarget.PARAMETERS) 注释。来自 ConstraintValidator 接口的类型参数 T 必须解析为 Object 或 Object[],以便在 isValid() 方法中接收方法/构造函数参数的数组。
In order to define a cross-parameter constraint, its validator class must be annotated with @SupportedValidationTarget(ValidationTarget.PARAMETERS). The type parameter T from the ConstraintValidator interface must resolve to either Object or Object[] in order to receive the array of method/constructor arguments in the isValid() method.
以下示例显示了一个交叉参数约束的定义,它可以用来检查方法的两个 Date 参数是否按正确的顺序排列:
The following example shows the definition of a cross-parameter constraint which can be used to check that two Date parameters of a method are in the correct order:
. Example 6.14: Cross-parameter constraint
package org.hibernate.validator.referenceguide.chapter06.crossparameter;
@Constraint(validatedBy = ConsistentDateParametersValidator.class)
@Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {
String message() default "{org.hibernate.validator.referenceguide.chapter04." +
"crossparameter.ConsistentDateParameters.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
跨参数约束的定义与定义通用约束没有任何区别,即它必须指定成员_message()、_groups()_和_payload(),并用_@Constraint_注解。此元注解还指定对应的验证器,如 Example 6.15, “Generic and cross-parameter constraint”中所示。请注意,除了元素类型_METHOD_和_CONSTRUCTOR_之外,还指定_ANNOTATION_TYPE_作为注解的目标,以基于_@ConsistentDateParameters_创建复合约束(请参见 Section 6.4, “Constraint composition”)。
The definition of a cross-parameter constraint isn’t any different from defining a generic constraint, i.e. it must specify the members message(), groups() and payload() and be annotated with @Constraint. This meta annotation also specifies the corresponding validator, which is shown in Example 6.15, “Generic and cross-parameter constraint”. Note that besides the element types METHOD and CONSTRUCTOR also ANNOTATION_TYPE is specified as target of the annotation, in order to enable the creation of composed constraints based on @ConsistentDateParameters (see Section 6.4, “Constraint composition”).
跨参数约束直接在方法或构造函数的声明中指定,返回值约束也是如此。为了提高代码可读性,因此建议选择约束名称(例如 @ConsistentDateParameters),它使约束目标显而易见。 |
Cross-parameter constraints are specified directly on the declaration of a method or constructor, which is also the case for return value constraints. In order to improve code readability, it is therefore recommended to choose constraint names - such as @ConsistentDateParameters - which make the constraint target apparent. |
. Example 6.15: Generic and cross-parameter constraint
package org.hibernate.validator.referenceguide.chapter06.crossparameter;
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParametersValidator implements
ConstraintValidator<ConsistentDateParameters, Object[]> {
@Override
public void initialize(ConsistentDateParameters constraintAnnotation) {
}
@Override
public boolean isValid(Object[] value, ConstraintValidatorContext context) {
if ( value.length != 2 ) {
throw new IllegalArgumentException( "Illegal method signature" );
}
//leave null-checking to @NotNull on individual parameters
if ( value[0] == null || value[1] == null ) {
return true;
}
if ( !( value[0] instanceof Date ) || !( value[1] instanceof Date ) ) {
throw new IllegalArgumentException(
"Illegal method signature, expected two " +
"parameters of type Date."
);
}
return ( (Date) value[0] ).before( (Date) value[1] );
}
}
如上所述,验证目标 PARAMETERS 必须使用 @SupportedValidationTarget 注释为交叉参数验证器配置。由于交叉参数约束可以应用于任何方法或构造函数,因此在验证器实现中检查预期的参数数量和类型被认为是一种最佳实践。
As discussed above, the validation target PARAMETERS must be configured for a cross-parameter validator by using the @SupportedValidationTarget annotation. Since a cross-parameter constraint could be applied to any method or constructor, it is considered a best practice to check for the expected number and types of parameters in the validator implementation.
与通用约束一样,null 参数应该被视为有效的,而且应该在各个参数上使用 @NotNull 来确保参数不是 null。
As with generic constraints, null parameters should be considered valid and @NotNull on the individual parameters should be used to make sure that parameters are not null.
与类级约束类似,可以在验证跨参数约束时在单个参数(而非所有参数)上创建自定义约束冲突。只要从传递给 isValid() 的 ConstraintValidatorContext 中获取一个节点构建器,并通过调用 addParameterNode() 添加一个参数节点。在该示例中,你可以使用它来针对已验证方法的结束日期参数创建约束冲突。 |
Similar to class-level constraints, you can create custom constraint violations on single parameters instead of all parameters when validating a cross-parameter constraint. Just obtain a node builder from the ConstraintValidatorContext passed to isValid() and add a parameter node by calling addParameterNode(). In the example you could use this to create a constraint violation on the end date parameter of the validated method. |
在罕见的情况下,约束既是通用的又是跨参数的。如果约束具有用 @SupportedValidationTarget({ValidationTarget.PARAMETERS, ValidationTarget.ANNOTATED_ELEMENT}) 注释的验证器类,或者如果它具有通用和跨参数验证器类,则会出现这种情况。
In rare situations a constraint is both, generic and cross-parameter. This is the case if a constraint has a validator class which is annotated with @SupportedValidationTarget({ValidationTarget.PARAMETERS, ValidationTarget.ANNOTATED_ELEMENT}) or if it has a generic and a cross-parameter validator class.
当对具有参数和返回值的方法声明此类约束时,无法确定预期约束目标。因此,通用和跨参数的约束必须定义允许约束用户指定约束目标的成员_validationAppliesTo()_,如 Example 6.16, “Generic and cross-parameter constraint”所示。
When declaring such a constraint on a method which has parameters and also a return value, the intended constraint target can’t be determined. Constraints which are generic and cross-parameter at the same time must therefore define a member validationAppliesTo() which allows the constraint user to specify the constraint’s target as shown in Example 6.16, “Generic and cross-parameter constraint”.
. Example 6.16: Generic and cross-parameter constraint
package org.hibernate.validator.referenceguide.chapter06.crossparameter;
@Constraint(validatedBy = {
ScriptAssertObjectValidator.class,
ScriptAssertParametersValidator.class
})
@Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ScriptAssert {
String message() default "{org.hibernate.validator.referenceguide.chapter04." +
"crossparameter.ScriptAssert.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String script();
ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
}
@ScriptAssert 约束有两个验证器(未显示),分别是通用和交叉参数验证器,因此定义了成员 validationAppliesTo()。默认值 IMPLICIT 允许在可能的情况下自动推导目标(例如,如果约束是在字段上声明的,或者是在具有参数但不具有返回值的方法上声明的)。
The @ScriptAssert constraint has two validators (not shown), a generic and a cross-parameter one and thus defines the member validationAppliesTo(). The default value IMPLICIT allows to derive the target automatically in situations where this is possible (e.g. if the constraint is declared on a field or on a method which has parameters but no return value).
如果无法隐式确定目标,则必须由用户将其设置为_PARAMETERS_或_RETURN_VALUE_,如 Example 6.17, “Specifying the target for a generic and cross-parameter constraint”所示。
If the target can not be determined implicitly, it must be set by the user to either PARAMETERS or RETURN_VALUE as shown in Example 6.17, “Specifying the target for a generic and cross-parameter constraint”.
. Example 6.17: Specifying the target for a generic and cross-parameter constraint
@ScriptAssert(script = "arg1.size() <= arg0", validationAppliesTo = ConstraintTarget.PARAMETERS)
public Car buildCar(int seatCount, List<Passenger> passengers) {
//...
return null;
}
6.4. Constraint composition
查看 Example 6.10, “Applying the @CheckCase constraint” 中 Car 类的 licensePlate 字段,您会看到三个约束注解。在更加复杂的情况下,更加多的约束可以应用于一个元素,这可能会容易引起一点困惑。此外,如果在另一个类中存在一个 licensePlate 字段,那么您将必须将所有约束声明复制到另一个类,违反 DRY 原则。
Looking at the licensePlate field of the Car class in Example 6.10, “Applying the @CheckCase constraint”, you see three constraint annotations already. In more complex scenarios, where even more constraints could be applied to one element, this might easily become a bit confusing. Furthermore, if there was a licensePlate field in another class, you would have to copy all constraint declarations to the other class as well, violating the DRY principle.
您可以通过创造更高级别的约束来解决此类问题,该约束由若干基本约束组成。 Example 6.18, “Creating a composing constraint @ValidLicensePlate” 显示了一个包含约束 @NotNull 、 @Size 和 @CheckCase 的组合约束注解:
You can address this kind of problem by creating higher level constraints, composed from several basic constraints. Example 6.18, “Creating a composing constraint @ValidLicensePlate” shows a composed constraint annotation which comprises the constraints @NotNull, @Size and @CheckCase:
. Example 6.18: Creating a composing constraint @ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;
@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
@Documented
public @interface ValidLicensePlate {
String message() default "{org.hibernate.validator.referenceguide.chapter06." +
"constraintcomposition.ValidLicensePlate.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
要创建组合约束,只需使用其组成约束对约束声明进行注释即可。如果组合约束本身需要验证器,则此验证器应在 @Constraint 注释中指定。对于不需要附加验证器的组合约束,例如 @ValidLicensePlate,只需将 validatedBy() 设置为一个空数组。
To create a composed constraint, simply annotate the constraint declaration with its comprising constraints. If the composed constraint itself requires a validator, this validator is to be specified within the @Constraint annotation. For composed constraints which don’t need an additional validator such as @ValidLicensePlate, just set validatedBy() to an empty array.
在 licensePlate 字段中使用新的组合约束与以前的版本完全等效,其中这三个约束直接在字段本身声明:
Using the new composed constraint at the licensePlate field is fully equivalent to the previous version, where the three constraints were declared directly at the field itself:
. Example 6.19: Application of composing constraint ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;
public class Car {
@ValidLicensePlate
private String licensePlate;
//...
}
ConstraintViolation_s retrieved when validating a _Car 实例的集合将包含 @ValidLicensePlate 约束的每个违反组合约束的条目。如果在任何组合约束被违反的情况下,您更喜欢单个 ConstraintViolation,可以使用 @ReportAsSingleViolation 元约束,如下所示:
The set of ConstraintViolation_s retrieved when validating a _Car instance will contain an entry for each violated composing constraint of the @ValidLicensePlate constraint. If you rather prefer a single ConstraintViolation in case any of the composing constraints is violated, the @ReportAsSingleViolation meta constraint can be used as follows:
. Example 6.20: Using @ReportAsSingleViolation
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition.reportassingle;
//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {
String message() default "{org.hibernate.validator.referenceguide.chapter06." +
"constraintcomposition.reportassingle.ValidLicensePlate.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}