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 сущности
Пример использования стандартных аннотаций валидации на полях сущности:
@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.
Как видите, приложение не только показывает пользователю сообщения об ошибках, но также выделяет красными линиями поля формы, которые не прошли bean-валидацию с одним полем.
Собственные ограничения
В проекте можно создать собственные ограничения с программной или декларативной валидацией.
Для создания ограничения с программной валидацией выполните следующее:
-
Создайте аннотацию:
@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
. -
Создайте класс валидатора:
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()
. -
Используйте аннотацию уровня класса:
@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
.
Экраны деталей сущностей выполняют валидацию на соответствие ограничениям уровня класса при коммите, если ограничение включает группу 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
}