Перейти к основному содержимому

Композиция ограничений с проверкой бина

· 5 мин. чтения

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 .