1. Обзор
В этом уроке мы обсудим композицию ограничений для проверки компонентов .
Группировка нескольких ограничений под одной пользовательской аннотацией может уменьшить дублирование кода и улучшить читабельность . Мы увидим, как создавать составные ограничения и как настраивать их в соответствии с нашими потребностями.
Для примеров кода у нас будут те же зависимости, что и в Java Bean Validation Basics .
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
повторяется для каждого из трех полей.
Кроме того, если одно из этих полей присутствует в нескольких классах из разных слоев, ограничения должны совпадать, что приводит к еще большему дублированию кода .
Например, мы можем представить себе наличие поля имени пользователя
в объекте DTO и модели @Entity
.
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
для проверки полей учетной записи
:
public class Account {
@ValidAlphanumeric
private String username;
@ValidAlphanumeric
private String password;
@ValidAlphanumeric
private String nickname;
// getters and setters
}
В результате мы можем протестировать аннотацию @ValidAlphanumeric
и ожидать столько же нарушений, сколько нарушенных ограничений.
Например, если мы установим имя пользователя
« 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 {};
}
После этого мы можем протестировать нашу новую аннотацию с помощью поля пароля
и ожидать единственного нарушения:
@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.
ИЛИ
:
@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 {};
}
Например, если значение слишком короткое, но содержит хотя бы один числовой символ, нарушения быть не должно.
Давайте протестируем эту новую аннотацию, используя поле псевдонима
из нашей модели:
@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 .