概述

在本教程中,我们将讨论Bean Validation中的约束组合。将多个约束合并到一个自定义注解下可以减少代码重复,并提高可读性。我们将学习如何创建复合约束以及如何根据需求定制它们。

本文示例代码依赖于Java Bean Validation基础中的相同依赖项。

2. 了解问题

首先,让我们熟悉数据模型。本文中的大多数例子都将使用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
}

我们可以注意到,每个字段上都重复出现了@NotNull, @Pattern@Length的一组约束。

此外,如果这些字段出现在不同层面上的多个类中,约束应该匹配,从而导致更多的代码重复。例如,我们可以想象username字段存在于DTO对象和实体模型中。

3. 创建复合约束

通过将三个约束放在一个具有合适名称的自定义注解下,我们可以避免代码复制:

@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 {};
}

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

public class Account {

    @ValidAlphanumeric
    private String username;

    @ValidAlphanumeric
    private String password;

    @ValidAlphanumeric
    private String nickname;

    // getters and setters
}

因此,我们可以测试@ValidAlphanumeric注解,并期望违反的约束数量与预期的违规行为相同。

例如,如果我们将username设置为"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. 使用@ReportAsSingleViolation

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

为了实现这一点,我们需要在我们的复合约束上添加@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 {};
}

之后,我们可以使用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. 布尔约束组合

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

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

要实现这一点,我们需要将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 {};
}

例如,对于一个长度太短但至少包含一个数字字符的值,应该没有违规。让我们使用模型中的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();
}

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

6. 使用复合约束进行方法验证

此外,我们可以将复合约束用作方法约束

为了验证方法的返回值,我们只需在复合约束上添加@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 {};
}

以示例说明,我们将使用带有我们自定义约束的getAnInvalidAlphanumericValue方法:

@Component
@Validated
public class AccountService {

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

现在,调用此方法并期待抛出一个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. 总结

在这篇文章中,我们学习了如何使用复合约束来避免代码重复。接下来,我们了解了如何定制复合约束以使用布尔逻辑进行验证,返回单个约束违规,并应用于方法返回值。如常,源代码可在GitHub上找到。