«1. Обзор

Версия 2.0 спецификации Java Bean Validation добавляет несколько новых функций, среди которых возможность проверки элементов контейнеров.

Эта новая функциональность использует аннотации типов, представленные в Java 8. Поэтому для работы требуется Java версии 8 или выше.

Аннотации проверки можно добавлять в такие контейнеры, как коллекции, необязательные объекты и другие встроенные, а также пользовательские контейнеры.

Для ознакомления с проверкой Java Bean Validation и тем, как настроить необходимые нам зависимости Maven, ознакомьтесь с нашей предыдущей статьей здесь.

В следующих разделах мы сосредоточимся на проверке элементов каждого типа контейнера.

2. Элементы коллекций

Мы можем добавлять аннотации проверки к элементам коллекций типа java.util.Iterable, java.util.List и java.util.Map.

Давайте рассмотрим пример проверки элементов списка:

public class Customer {    
     List<@NotBlank(message="Address must not be blank") String> addresses;
    
    // standard getters, setters 
}

В приведенном выше примере мы определили свойство address для класса Customer, которое содержит элементы, которые не могут быть пустыми строками.

Обратите внимание, что проверка @NotBlank применяется к элементам String, а не ко всей коллекции. Если коллекция пуста, проверка не применяется.

Давайте проверим, что если мы попытаемся добавить пустую строку в список адресов, среда проверки вернет ConstraintViolation:

@Test
public void whenEmptyAddress_thenValidationFails() {
    Customer customer = new Customer();
    customer.setName("John");

    customer.setAddresses(Collections.singletonList(" "));
    Set<ConstraintViolation<Customer>> violations = 
      validator.validate(customer);
    
    assertEquals(1, violations.size());
    assertEquals("Address must not be blank", 
      violations.iterator().next().getMessage());
}

Далее, давайте посмотрим, как мы можем проверить элементы коллекции типа Map:

public class CustomerMap {
    
    private Map<@Email String, @NotNull Customer> customers;
    
    // standard getters, setters
}

Обратите внимание, что мы можем добавить аннотации проверки как для ключа, так и для значения элемента Map.

Давайте проверим, что добавление записи с недопустимым адресом электронной почты приведет к ошибке проверки:

@Test
public void whenInvalidEmail_thenValidationFails() {
    CustomerMap map = new CustomerMap();
    map.setCustomers(Collections.singletonMap("john", new Customer()));
    Set<ConstraintViolation<CustomerMap>> violations
      = validator.validate(map);
 
    assertEquals(1, violations.size());
    assertEquals(
      "Must be a valid email", 
      violations.iterator().next().getMessage());
}

3. Необязательные значения

Ограничения проверки также могут быть применены к необязательному значению:

private Integer age;

public Optional<@Min(18) Integer> getAge() {
    return Optional.ofNullable(age);
}

Давайте создадим клиента со слишком низким возрастом — и убедимся, что это приводит к ошибке проверки:

@Test
public void whenAgeTooLow_thenValidationFails() {
    Customer customer = new Customer();
    customer.setName("John");
    customer.setAge(15);
    Set<ConstraintViolation<Customer>> violations
      = validator.validate(customer);
 
    assertEquals(1, violations.size());
}

С другой стороны, если возраст равен нулю, то необязательное значение не проверяется: ~ ~~

@Test
public void whenAgeNull_thenValidationSucceeds() {
    Customer customer = new Customer();
    customer.setName("John");
    Set<ConstraintViolation<Customer>> violations
      = validator.validate(customer);
 
    assertEquals(0, violations.size());
}

4. Элементы неуниверсального контейнера

Помимо добавления аннотаций для аргументов типа, мы также можем применять проверку к неуниверсальным контейнерам, если для типа имеется экстрактор значений с аннотацией @UnwrapByDefault.

Экстракторы значений — это классы, которые извлекают значения из контейнеров для проверки.

Эталонная реализация содержит экстракторы значений для OptionalInt,OptionalLong иOptionalDouble:

@Min(1)
private OptionalInt numberOfOrders;

В этом случае аннотация @Min применяется к обернутому значению Integer, а не к контейнеру.

5. Пользовательские элементы контейнера

В дополнение к встроенным экстракторам значений мы также можем определить свои собственные и зарегистрировать их в типе контейнера.

Таким образом, мы можем добавлять аннотации проверки к элементам наших пользовательских контейнеров.

Давайте добавим новый класс Profile, который содержит свойство companyName:

public class Profile {
    private String companyName;
    
    // standard getters, setters 
}

Затем мы хотим добавить свойство Profile в класс Customer с аннотацией @NotBlank, которая проверяет, что companyName не является пустым. Строка:

@NotBlank
private Profile profile;

Чтобы это работало, нам нужен экстрактор значений, который определяет проверку, которая будет применяться к свойству companyName, а не непосредственно к объекту профиля.

Давайте добавим класс ProfileValueExtractor, реализующий интерфейс ValueExtractor и переопределяющий метод ExtractValue():

@UnwrapByDefault
public class ProfileValueExtractor 
  implements ValueExtractor<@ExtractedValue(type = String.class) Profile> {

    @Override
    public void extractValues(Profile originalValue, 
      ValueExtractor.ValueReceiver receiver) {
        receiver.value(null, originalValue.getCompanyName());
    }
}

Этот класс также должен указывать тип извлекаемого значения с помощью аннотации @ExtractedValue.

Кроме того, мы добавили аннотацию @UnwrapByDefault, указывающую, что проверка должна применяться к развернутому значению, а не к контейнеру.

Наконец, нам нужно зарегистрировать класс, добавив файл с именем javax.validation.valueextraction.ValueExtractor в каталог META-INF/services, который содержит полное имя нашего класса ProfileValueExtractor:

org.baeldung.javaxval.container.validation.valueextractors.ProfileValueExtractor

Теперь, когда мы проверяем объект Customer со свойством профиля с пустым companyName, мы увидим ошибку проверки:

@Test
public void whenProfileCompanyNameBlank_thenValidationFails() {
    Customer customer = new Customer();
    customer.setName("John");
    Profile profile = new Profile();
    profile.setCompanyName(" ");
    customer.setProfile(profile);
    Set<ConstraintViolation<Customer>> violations
     = validator.validate(customer);
 
    assertEquals(1, violations.size());
}

«

«Обратите внимание, что если вы используете hibernate-validator-annotation-processor, добавление аннотации проверки к пользовательскому классу контейнера, когда он помечен как @UnwrapByDefault, приведет к ошибке компиляции в версии 6.0.2.

Это известная проблема, которая, скорее всего, будет решена в следующей версии.

6. Заключение

В этой статье мы показали, как мы можем проверять несколько типов элементов контейнера с помощью Java Bean Validation 2.0.