Bean Validation

Java Bean Validation — это спецификация для валидации данных в приложениях на Java. Текущая версия 2.0 спецификации доступна здесь. Эталонной реализацией 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 = "PERSON", indexes = {
        @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
})
@Entity
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 = "PERSON", indexes = {
            @Index(name = "IDX_PERSON_LOCATION_ID", columnList = "LOCATION_ID")
    })
    @Entity
    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://com.company.demo.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.

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

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

Компоненты datePicker, dateTimePicker и timePicker автоматически устанавливают свой допустимый диапазон в соответствии с аннотациями @Past, @PastOrPresent, @Future, @FutureOrPresent.

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

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

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

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

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

@Autowired
private Validator validator;

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