Bean Validation

Bean Validation — это механизм для валидации данных. Текущая версия — 2.0, описанная в JSR-380. Эталонной реализацией Bean Validation является Hibernate Validator.

Использование подхода Bean Validation дает вашему проекту довольно немало преимуществ:

  • Логика валидации расположена рядом с предметной областью: определение ограничений для полей и методов бина происходит естественным и по-настоящему объектно-ориентированным образом.

  • Стандарт Bean Validation дает вам десятки валидационных аннотаций прямо из коробки, например: @NotNull, @Size, @Min, @Max, @Pattern, @Email, @Past, не совсем стандартные @URL, @Length, и многие другие.

  • Вы не ограничены предопределенными ограничениями и можете определять свои собственные аннотации ограничений. Вы также можете создать новую аннотацию, объединив несколько других, или создать совершенно новую и определить класс Java, который будет служить в качестве средства проверки.

    Например, вы можете определить аннотацию на уровне класса @ValidPassportNumber, чтобы проверить, соответствует ли номер паспорта правильному формату, который зависит от значения поля location.

  • Ограничения можно ставить не только на поля или классы, но и на методы и их параметры. Этот подход называется "validation by contract".

Bean Validation вызывается автоматически в экранах UI, когда пользователь отправляет введенную информацию, а также в универсальном REST API.

Определение ограничений

Ограничения Bean Validation определяются с помощью аннотаций пакета javax.validation.constraints или собственных аннотаций. Аннотации указываются на декларации класса сущности или POJO, на поле или getter-методе, а также на методе сервиса.

Стандартный набор ограничений включает наиболее часто используемые и универсальные. Кроме того, Bean Validation позволяет разработчикам добавлять собственные ограничения.

  • @NotNull проверяет, что значение аннотированного свойства не равно null.

  • @Size проверяет, что значение аннотированного свойства имеет размер между атрибутами min и ; max может применяться к свойствам String, Collection, Map, и массивам.

  • @Min проверяет, что аннотированное свойство имеет значение выше или равное атрибуту value.

  • @Max проверяет, что аннотированное свойство имеет значение, меньшее или равное атрибуту value.

  • @Email проверяет, что аннотированное свойство является допустимым адресом электронной почты.

  • @NotEmpty проверяет, что свойство не является null или пустым; может применяться к значениям String`, Collection, Map или Array.

  • @NotBlank может применяться только к текстовым значениям и проверяет, что значение свойства не является null или пробельным символом.

  • @Positive и @PositiveOrZero применяются к числовым значениям и проверяют, что они строго положительны или положительны, включая 0.

  • @Negative и @NegativeOrZero применяются к числовым значениям и проверяют, что они строго отрицательные или отрицательные, включая 0.

  • @Past и @PastOrPresent проверяют, что значение даты находится в прошлом или в прошлом, включая настоящее.

  • @Future и @FutureOrPresent проверяют, что значение даты находится в будущем или в будущем, включая настоящее.

  • @Pattern проверяет, соответствует ли свойство аннотированной строки регулярному выражению regex.

Bean Validation сущности

Пример использования стандартных аннотаций валидации на полях сущности:

Person.java
@JmixEntity
@Table(name = "SAMPLE_PERSON", indexes = {
        @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
})
@Entity(name = "sample_Person")
public class Person {
    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @InstanceName
    @Length(min = 3) (1)
    @Column(name = "FIRST_NAME", nullable = false)
    @NotNull
    private String firstName;

    @Email(message = "Email address has invalid format: ${validatedValue}",
            regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") (2)
    @Column(name = "EMAIL", length = 120)
    private String email;

    @DecimalMin(message = "Person height should be positive",
            value = "0", inclusive = false) (3)
    @DecimalMax(message = "Person height can not exceed 300 centimeters",
            value = "300") (4)
    @Column(name = "HEIGHT", precision = 19, scale = 2)
    private BigDecimal height;

    @Column(name = "PASSPORT_NUMBER", nullable = false, length = 15)
    @NotNull
    private String passportNumber;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "LOCATION_ID")
    private Location location;
}
1 Длина имени человека должна быть более 3 символов.
2 Строка электронной почты должна быть адресом электронной почты в правильном формате.
3 Рост человека должен быть больше 0.
4 Рост человека должен быть меньше или равен 300.

Давайте проверим, как автоматически выполняется валидация бина, когда пользователь отправляет данные в UI.

validation ui

Как видите, приложение не только показывает пользователю сообщения об ошибках, но также выделяет красными линиями поля формы, которые не прошли bean-валидацию с одним полем.

Собственные ограничения

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

Для создания ограничения с программной валидацией выполните следующее:

  1. Создайте аннотацию:

    @Target(ElementType.TYPE) (1)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = ValidPassportNumberValidator.class) (2)
    public @interface ValidPassportNumber {
        String message() default "Passport number is not valid";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    1 Определяет, что целью этой аннотации времени выполнения является класс или интерфейс.
    2 Указывает, что реализация аннотации находится в классе ValidPassportNumberValidator.
  2. Создайте класс валидатора:

    public class ValidPassportNumberValidator
            implements ConstraintValidator<ValidPassportNumber, Person> {
    
        @Override
        public boolean isValid(Person person, ConstraintValidatorContext context) { (1)
            if (person == null)
                return false;
    
            if (person.getLocation() == null || person.getPassportNumber() == null)
                return false;
    
            return doPassportNumberFormatCheck(person.getLocation(),
                    person.getPassportNumber());
        }
    }
    1 Фактически, проверку выполняет метод isValid().
  3. Используйте аннотацию уровня класса:

    @ValidPassportNumber(groups = {Default.class, UiCrossFieldChecks.class})
    @JmixEntity
    @Table(name = "SAMPLE_PERSON", indexes = {
            @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
    })
    @Entity(name = "sample_Person")
    public class Person {
    }

Собственные аннотации могут также быть созданы как композиции имеющихся, например:

@NotNull
@Size(min = 2, max = 14)
@Pattern(regexp = "\\d+")
@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface ValidZipCode {
    String message() default "Zip code is not valid";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

При использовании композитных ограничений результирующий набор нарушений будет содержать отдельные записи для каждого включенного ограничения. Для того чтобы получить одну запись нарушения, добавьте ReportAsSingleViolation классу вашей аннотации.

Валидация по контракту

При Bean Validation ограничения могут применяться к входным параметрам и возвращаемым значениям методов и конструкторов для проверки предусловий и постусловий их вызовов у любого Java класса. Это называется «validation by contract».

Благодаря подходу «валидация по контракту» вы получаете понятный, компактный и легко поддерживаемый код.

Службы выполняют валидацию параметров и результатов, если метод имеет аннотацию @Validated в интерфейсе службы. Например:

@Validated
public interface PersonApiService {

    String NAME = "sample_PersonApiService";

    @NotNull
    @Valid (1)
    List<Person> getPersons();

    void addNewPerson(@NotNull
                      @Length(min = 3)
                      String firstName,
                      @DecimalMax(message = "Person height can not exceed 300 centimeters",
                              value = "300")
                      @DecimalMin(message = "Person height should be positive",
                              value = "0", inclusive = false)
                      BigDecimal height,
                      @NotNull
                      String passportNumber
    );

    @Validated (2)
    @NotNull
    String validatePerson(@Size(min = 5) String comment,
                          @Valid @NotNull Person person); (3)
}
1 Указывает, что каждый объект в списке, возвращаемом методом getPersons(), также должен быть проверен на соответствие ограничениям класса Person.
2 Указывает, что метод должен быть проверен.
3 Аннотацию @Valid можно использовать, если вам нужна каскадная валидация параметров метода. В приведенном выше примере также будут проверены ограничения, объявленные для объекта Person.

Если вы выполняете собственную программную валидацию в службе, используйте CustomValidationException для информирования клиентов об ошибках валидации в том же формате, что и стандартная Bean Validation. Это может быть особенно актуально для клиентов REST API.

Bean Validation наследуется. Если вы аннотируете какой-либо класс, поле или метод ограничением, все наследники, которые расширяют или реализуют этот класс или интерфейс, будут затронуты одной и той же проверкой ограничения.

Группы ограничений

Группы ограничений позволяют применять только часть всех определенных ограничений в зависимости от логики приложения. Например, возможно, вы захотите заставить пользователя ввести значение атрибута сущности, но имея возможность установить этот атрибут в null с помощью какого-то внутреннего механизма. Для этого необходимо указать атрибут группы groups в аннотации ограничения. Тогда ограничение вступит в силу только тогда, когда та же группа будет передана механизму валидации.

Фреймворк передает следующие группы ограничений механизму валидации:

  • RestApiChecks - группа ограничений bean validation, используемая REST API для валидации данных.

  • UiComponentChecks - группа ограничений bean validation, используемая пользовательским интерфейсом для валидации полей.

  • UiCrossFieldChecks - группа ограничений bean validation, используемая пользовательским интерфейсом для перекрестной валидации.

  • javax.validation.groups.Default - эта группа передается всегда, кроме как при коммите редактора UI.

Cообщения валидации

У ограничений могут быть сообщения для отображения пользователям.

Сообщения можно задавать прямо в валидационных аннотациях, например:

@Pattern(message = "Bad formed person last name: ${validatedValue}",
        regexp = "^[A-Z][a-z]*(\\s(([a-z]{1,3})|(([a-z]+\\')?[A-Z][a-z]*)))*$")
@Column(name = "LAST_NAME", nullable = false)
@NotNull
private String lastName;

Сообщение также можете поместить в пакет сообщений и указать ключ сообщения в аннотации. Например:

@Min(message = "{msg://datamodel.ex1.entity/Person.age.validation.Min}", value = 14)
@Column(name = "AGE")
private Integer age;

Сообщения могут содержать параметры и выражения. Параметры заключены в {} и представляют собой либо локализованные сообщения, либо параметры аннотаций, например {min}, {max}, {value}. Выражения заключены в ${} и могут включать переменную проверенного значения validatedValue, параметры аннотации, такие как value или min, и выражения JSR-341 (EL 3.0). Например:

@Pattern(message = "Invalid name: ${validatedValue}, pattern: {regexp}",
        regexp = "^[A-Z][a-z]*(\\s(([a-z]{1,3})|(([a-z]+\\')?[A-Z][a-z]*)))*$")
@Column(name = "FULL_NAME")
private String fullName;

Локализованные значения сообщений также могут содержать параметры и выражения.

Выполнение валидации

Валидация в UI

Компоненты пользовательского интерфейса, связанные с данными, автоматически получают BeanPropertyValidator для проверки значения поля. Валидатор вызывается из метода Validatable.validate(), реализованного визуальным компонентом, и может выбросить исключение ValidationException.

Стандартный валидатор можно удалить или инициализировать с другой группой ограничений:

@UiController("sample_Person.edit")
@UiDescriptor("person-edit.xml")
@EditedEntityContainer("personDc")
public class PersonEdit extends StandardEditor<Person> {

    @Autowired
    private TextField<String> passportNumberField;

    @Subscribe("removeValidator")
    public void onRemoveValidator(Action.ActionPerformedEvent event) {
        Collection<? extends Validator<?>> validators =
                passportNumberField.getValidators();

        for (Validator validator : validators.toArray(new Validator[0])) {
            if (validator instanceof BeanPropertyValidator) {
                passportNumberField.removeValidator(validator); (1)
            }
        }
    }

    @Subscribe("setValidGroups")
    public void onSetValidGroups(Action.ActionPerformedEvent event) {
        Collection<? extends Validator<?>> validators =
                passportNumberField.getValidators();

        for (Validator validator : validators.toArray(new Validator[0])) {
            if (validator instanceof BeanPropertyValidator) {
                ((BeanPropertyValidator) validator).setValidationGroups(
                        new Class[] {UiComponentChecks.class}); (2)
            }
        }
    }
}
1 Полностью удаляет bean validation из компонента UI.
2 Здесь валидаторы проверяют только ограничения с явно заданной группой UiComponentChecks, потому что группа Default передана не будет.

По умолчанию AbstractBeanValidator имеет обе группы Default и UiComponentChecks.

Если атрибут сущности аннотирован @NotNull без групп ограничений, он будет помечен как обязательный в метаданных, а компоненты пользовательского интерфейса, связанные с данными, будут иметь required = true.

Компоненты DateField и DatePicker автоматически устанавливают свои свойства rangeStart и rangeEnd с помощью аннотаций @Past, @PastOrPresent, @Future, @FutureOrPresent.

Экраны редактора сущностей выполняют валидацию на соответствие ограничениям уровня класса при коммите, если ограничение включает группу UiCrossFieldChecks и если все проверки на уровне атрибутов пройдены. Вы можете отключить валидацию такого рода с помощью метода контроллера setCrossFieldValidate():

@UiController("sample_Person.edit")
@UiDescriptor("person-edit.xml")
@EditedEntityContainer("personDc")
public class PersonEdit extends StandardEditor<Person> {

    @Subscribe("cancelCrossFValidate")
    public void onCancelCrossFValidate(Action.ActionPerformedEvent event) {
        setCrossFieldValidate(false);
    }
}

Валидация в REST API

Универсальный REST API автоматически выполняет Bean Validation для создания и обновления действий, а также при использовании подхода Services API.

Программная валидация

Вы можете выполнить Bean Validation программно, используя метод validate() интерфейса javax.validation.Validator. Результатом валидации является набор объектов ConstraintViolation. Например:

@Autowired
protected Validator validator;

protected void save(Person person) {
    Set<ConstraintViolation<Person>> violations = validator.validate(person);
    /*
    handling of the returned collection of violations
    */
}