概述
在本教程中,我们将讨论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上找到。