Constraint Composition with Bean Validation – 带有Bean验证的约束构成

最后修改: 2022年 5月 22日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll discuss Constraint Composition for Bean Validation.

在本教程中,我们将讨论用于Bean Validation的约束条件组合。

Grouping multiple constraints under a single, custom annotation can reduce code duplication and improve readability. We’ll see how to create composed constraints and how to customize them according to our needs.

在一个单一的、自定义的注解下组合多个约束,可以减少代码的重复,提高可读性。我们将看到如何创建组成的约束,以及如何根据我们的需要定制它们。

For the code examples, we’ll have the same dependencies as in Java Bean Validation Basics.

对于代码示例,我们将拥有与Java Bean验证基础中相同的依赖性。

2. Understanding the Problem

2.了解问题

Firstly, let’s get familiar with the data model. We’ll use the Account class for the majority of the examples in this article:

首先,让我们熟悉一下数据模型。我们将使用Account类来完成本文中的大部分例子:

public class Account {

    @NotNull
    @Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
    @Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
    private String username;
	
    @NotNull
    @Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
    @Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
    private String nickname;
	
    @NotNull
    @Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
    @Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
    private String password;

    // getters and setters
}

We can notice the group of @NotNull, @Pattern, and @Length constraints being repeated for each of the three fields.

我们可以注意到@NotNull, @Pattern, @Length这组约束在三个字段中的每一个都被重复。

Furthermore, if one of these fields is present in multiple classes from different layers, the constraints should match – leading to even more code duplication.

此外,如果这些字段之一出现在不同层的多个类中,那么约束条件应该匹配 – 导致更多的代码重复

For example, we can imagine having the username field in a DTO object and the @Entity model.

例如,我们可以想象在一个DTO对象和@Entity模型中拥有username字段。

3. Creating a Composed Constraint

3.创建一个组合式约束

We can avoid the code duplication by grouping the three constraints under a custom annotation with a suitable name:

我们可以通过将这三个约束归入一个具有合适名称的自定义注解来避免代码的重复。

@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
public @interface ValidAlphanumeric {

    String message() default "field should have a valid length and contain numeric character(s).";

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

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

Consequently, we can now use @ValidAlphanumeric to validate Account fields:

因此,我们现在可以使用@ValidAlphanumeric来验证Account字段。

public class Account {

    @ValidAlphanumeric
    private String username;

    @ValidAlphanumeric
    private String password;

    @ValidAlphanumeric
    private String nickname;

    // getters and setters
}

As a result, we can test the @ValidAlphanumeric annotation and expect as many violations as violated constraints.

因此,我们可以测试@ValidAlphanumeric注解,并期待有多少违反的约束就有多少违反。

For instance, if we set the username to “john”, we should expect two violations because it’s both too short and doesn’t contain a numeric character:

例如,如果我们将用户名设置为“john”,我们应该预期有两次违规,因为它既太短又不包含一个数字字符。

@Test
public void whenUsernameIsInvalid_validationShouldReturnTwoViolations() {
    Account account = new Account();
    account.setPassword("valid_password123");
    account.setNickname("valid_nickname123");
    account.setUsername("john");

    Set<ConstraintViolation<Account>> violations = validator.validate(account);

    assertThat(violations).hasSize(2);
}

4. Using @ReportAsSingleViolation

4.使用@ReportAsingleViolation

On the other hand, we may want the validation to return a single ConstraintViolation for the whole group.

另一方面,我们可能希望验证能够为整个组返回一个单一的ConstraintViolation

To achieve this, we have to annotate our composed constraint with @ReportAsSingleViolation:

为了实现这一点,我们必须用@ReportAsSingleViolation来注释我们的组成约束。

@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface ValidAlphanumericWithSingleViolation {

    String message() default "field should have a valid length and contain numeric character(s).";

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

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

After that, we can test our new annotation using the password field and expect a single violation:

之后,我们可以使用password字段来测试我们的新注解,并期待有一次违规。

@Test
public void whenPasswordIsInvalid_validationShouldReturnSingleViolation() {
    Account account = new Account();
    account.setUsername("valid_username123");
    account.setNickname("valid_nickname123");
    account.setPassword("john");

    Set<ConstraintViolation<Account>> violations = validator.validate(account);

    assertThat(violations).hasSize(1);
}

5. Boolean Constraint Composition

5.布尔约束的构成

So far, the validations passed only when all the composing constraints were valid. This is happening because the ConstraintComposition value defaults to CompositionType.AND

到目前为止,只有当所有的组合约束都有效时,验证才会通过。这是因为ConstraintComposition值默认为CompositionType.and

However, we can change this behavior if we want to check if there is at least one valid constraint.

然而,如果我们想检查是否有至少一个有效的约束,我们可以改变这种行为。

To achieve this, we need to switch ConstraintComposition to CompositionType.OR:

为了实现这一点,我们需要将ConstraintComposition切换为CompositionType.OR

@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@ConstraintComposition(CompositionType.OR)
public @interface ValidLengthOrNumericCharacter {

    String message() default "field should have a valid length or contain numeric character(s).";

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

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

For example, given a value that is too short but has at least one numerical character, there should be no violation.

例如,给定一个太短但至少有一个数字字符的值,应该没有违规。

Let’s test this new annotation using the nickname field from our model:

让我们使用我们模型中的nickname字段来测试这个新注解。

@Test
public void whenNicknameIsTooShortButContainsNumericCharacter_validationShouldPass() {
    Account account = new Account();
    account.setUsername("valid_username123");
    account.setPassword("valid_password123");
    account.setNickname("doe1");

    Set<ConstraintViolation<Account>> violations = validator.validate(account);

    assertThat(violations).isEmpty();
}

Similarly, we can use CompositionType.ALL_FALSE if we want to ensure the constraints are failing.

同样地,如果我们想确保约束失败,我们可以使用CompositionType.ALL_FALSE

6. Using Composed Constraints for Method Validation

6.使用组合式约束条件进行方法验证

Moreover, we can use composed constraints as method constraints.

此外,我们可以将组成的约束作为方法约束

In order to validate a method’s return value, we simply need to add @SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT) to the composed constraint:

为了验证一个方法的返回值,我们只需要在组成的约束中加入@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)

@NotNull
@Pattern(regexp = ".*\\d.*", message = "must contain at least one numeric character")
@Length(min = 6, max = 32, message = "must have between 6 and 32 characters")
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
public @interface AlphanumericReturnValue {

    String message() default "method return value should have a valid length and contain numeric character(s).";

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

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

To exemplify this, we’ll use the getAnInvalidAlphanumericValue method, which is annotated with our custom constraint:

为了说明这一点,我们将使用getAnInvalidAlphanumericValue方法,它被注释为我们的自定义约束。

@Component
@Validated
public class AccountService {

    @AlphanumericReturnValue
    public String getAnInvalidAlphanumericValue() {
        return "john"; 
    }
}

Now, let’s call this method and expect a ConstraintViolationException to be thrown:

现在,让我们调用这个方法并期待抛出一个ConstraintViolationException

@Test
public void whenMethodReturnValuesIsInvalid_validationShouldFail() {
    assertThatThrownBy(() -> accountService.getAnInvalidAlphanumericValue())				 
      .isInstanceOf(ConstraintViolationException.class)
      .hasMessageContaining("must contain at least one numeric character")
      .hasMessageContaining("must have between 6 and 32 characters");
}

7. Conclusion

7.结语

In this article, we’ve seen how to avoid code duplication using composed constraints.

在这篇文章中,我们已经看到了如何使用组成的约束来避免代码的重复。

After that, we learned to customize the composed constraint to use boolean logic for the validation, to return a single constraint violation, and to be applied to method return values.

之后,我们学会了自定义组成的约束,以使用布尔逻辑进行验证,返回单个约束的违反情况,并应用于方法返回值。

As always, the source code is available over on GitHub.

一如既往,源代码可在GitHub上获取。