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

Ограничения метода с проверкой бина 2.0

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

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

В этой статье мы обсудим, как определить и проверить ограничения метода с помощью Bean Validation 2.0 (JSR-380).

В предыдущей статье мы обсудили JSR-380 с его встроенными аннотациями и как реализовать проверку свойств.

Здесь мы сосредоточимся на различных типах ограничений метода, таких как:

  • однопараметрические ограничения
  • перекрестный параметр
  • возвращаемые ограничения

Кроме того, мы рассмотрим, как проверять ограничения вручную и автоматически с помощью Spring Validator.

Для следующих примеров нам нужны точно такие же зависимости, как и в Java Bean Validation Basics .

2. Объявление ограничений метода

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

Как упоминалось ранее, мы можем использовать аннотации из javax.validation.constraints , но мы также можем указать пользовательские ограничения (например, для пользовательских ограничений или ограничений перекрестных параметров).

2.1. Ограничения одного параметра

Определить ограничения для отдельных параметров несложно. Нам просто нужно добавить аннотации к каждому параметру по мере необходимости :

public void createReservation(@NotNull @Future LocalDate begin,
@Min(1) int duration, @NotNull Customer customer) {

// ...
}

Точно так же мы можем использовать тот же подход для конструкторов:

public class Customer {

public Customer(@Size(min = 5, max = 200) @NotNull String firstName,
@Size(min = 5, max = 200) @NotNull String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

// properties, getters, and setters
}

2.2. Использование ограничений перекрестных параметров

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

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

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

Рассмотрим простой пример: вариант метода createReservation() из предыдущего раздела принимает два параметра типа LocalDate: дату начала и дату окончания.

Следовательно, мы хотим убедиться, что начало находится в будущем, а конец — после начала . В отличие от предыдущего примера, мы не можем определить это с помощью ограничений одного параметра.

Вместо этого нам нужно перекрестное ограничение параметров.

В отличие от однопараметрических ограничений, межпараметрические ограничения объявляются в методе или конструкторе :

@ConsistentDateParameters
public void createReservation(LocalDate begin,
LocalDate end, Customer customer) {

// ...
}

2.3. Создание ограничений перекрестных параметров

Чтобы реализовать ограничение @ConsistentDateParameters , нам нужно выполнить два шага.

Во- первых, нам нужно определить аннотацию ограничения :

@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

String message() default
"End date must be after begin date and both must be in the future";

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

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

Здесь эти три свойства являются обязательными для аннотаций ограничений:

  • message — возвращает ключ по умолчанию для создания сообщений об ошибках, это позволяет нам использовать интерполяцию сообщений
  • группы — позволяет нам указать группы проверки для наших ограничений
  • полезная нагрузка — может использоваться клиентами Bean Validation API для назначения настраиваемых объектов полезной нагрузки ограничению.

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

После этого мы можем определить класс валидатора:

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
implements ConstraintValidator<ConsistentDateParameters, Object[]> {

@Override
public boolean isValid(
Object[] value,
ConstraintValidatorContext context) {

if (value[0] == null || value[1] == null) {
return true;
}

if (!(value[0] instanceof LocalDate)
|| !(value[1] instanceof LocalDate)) {
throw new IllegalArgumentException(
"Illegal method signature, expected two parameters of type LocalDate.");
}

return ((LocalDate) value[0]).isAfter(LocalDate.now())
&& ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
}
}

Как мы видим, метод isValid() содержит фактическую логику проверки. Во-первых, мы убеждаемся, что получаем два параметра типа LocalDate. После этого мы проверяем, находятся ли оба в будущем, а конец находится после начала .

Кроме того, важно отметить, что аннотация @SupportedValidationTarget(ValidationTarget . PARAMETERS) в классе ConsistentDateParameterValidator является обязательной. Причина этого в том, что @ConsistentDateParameter устанавливается на уровне метода, но ограничения должны применяться к параметрам метода (а не к возвращаемому значению метода, как мы обсудим в следующем разделе).

Примечание. Спецификация Bean Validation рекомендует рассматривать нулевые значения как допустимые. Если null не является допустимым значением, вместо него следует использовать аннотацию @NotNull .

2.4. Ограничения возвращаемого значения

Иногда нам нужно проверять объект, когда он возвращается методом. Для этого мы можем использовать ограничения возвращаемого значения.

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

public class ReservationManagement {

@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
}

Для getAllCustomers() применяются следующие ограничения:

  • Во-первых, возвращаемый список не должен быть нулевым и должен содержать хотя бы одну запись .
  • Кроме того, список не должен содержать пустых записей .

2.5. Пользовательские ограничения возвращаемого значения

В некоторых случаях нам также может понадобиться проверить сложные объекты:

public class ReservationManagement {

@ValidReservation
public Reservation getReservationsById(int id) {
return null;
}
}

В этом примере возвращаемый объект Reservation должен удовлетворять ограничениям, определенным @ValidReservation , которые мы определим далее.

Опять же, сначала мы должны определить аннотацию ограничения :

@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
String message() default "End date must be after begin date "
+ "and both must be in the future, room number must be bigger than 0";

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

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

После этого мы определяем класс валидатора:

public class ValidReservationValidator
implements ConstraintValidator<ValidReservation, Reservation> {

@Override
public boolean isValid(
Reservation reservation, ConstraintValidatorContext context) {

if (reservation == null) {
return true;
}

if (!(reservation instanceof Reservation)) {
throw new IllegalArgumentException("Illegal method signature, "
+ "expected parameter of type Reservation.");
}

if (reservation.getBegin() == null
|| reservation.getEnd() == null
|| reservation.getCustomer() == null) {
return false;
}

return (reservation.getBegin().isAfter(LocalDate.now())
&& reservation.getBegin().isBefore(reservation.getEnd())
&& reservation.getRoom() > 0);
}
}

2.6. Возвращаемое значение в конструкторах

Поскольку ранее мы определили METHOD и CONSTRUCTOR как цель в нашем интерфейсе ValidReservation , мы также можем аннотировать конструктор Reservation для проверки сконструированных экземпляров :

public class Reservation {

@ValidReservation
public Reservation(
LocalDate begin,
LocalDate end,
Customer customer,
int room) {
this.begin = begin;
this.end = end;
this.customer = customer;
this.room = room;
}

// properties, getters, and setters
}

2.7. Каскадная проверка

Наконец, Bean Validation API позволяет нам проверять не только отдельные объекты, но и графы объектов, используя так называемую каскадную проверку.

Следовательно, мы можем использовать @Valid для каскадной проверки, если мы хотим проверить сложные объекты . Это работает как для параметров метода, так и для возвращаемых значений.

Предположим, что у нас есть класс Customer с некоторыми ограничениями свойств:

public class Customer {

@Size(min = 5, max = 200)
private String firstName;

@Size(min = 5, max = 200)
private String lastName;

// constructor, getters and setters
}

Класс Reservation может иметь свойство Customer , а также другие свойства с ограничениями:

public class Reservation {

@Valid
private Customer customer;

@Positive
private int room;

// further properties, constructor, getters and setters
}

Если теперь мы ссылаемся на Reservation как параметр метода, мы можем принудительно выполнить рекурсивную проверку всех свойств :

public void createNewCustomer(@Valid Reservation reservation) {
// ...
}

Как мы видим, мы используем @Valid в двух местах:

  • В параметре резервирования : он запускает проверку объекта резервирования , когда вызывается createNewCustomer () .
  • Поскольку здесь у нас есть вложенный граф объектов, мы также должны добавить @Valid к атрибуту клиента : тем самым он запускает проверку этого вложенного свойства.

Это также работает для методов, возвращающих объект типа Reservation :

@Valid
public Reservation getReservationById(int id) {
return null;
}

3. Проверка ограничений метода

После объявления ограничений в предыдущем разделе мы можем перейти к фактической проверке этих ограничений. Для этого у нас есть несколько подходов.

3.1. Автоматическая проверка с помощью Spring

Spring Validation обеспечивает интеграцию с Hibernate Validator.

Примечание. Проверка Spring основана на AOP и использует Spring AOP в качестве реализации по умолчанию. Поэтому проверка работает только для методов, но не для конструкторов.

Если теперь мы хотим, чтобы Spring автоматически проверял наши ограничения, нам нужно сделать две вещи:

Во-первых, мы должны аннотировать bean-компоненты, которые должны быть проверены, с помощью @Validated :

@Validated
public class ReservationManagement {

public void createReservation(@NotNull @Future LocalDate begin,
@Min(1) int duration, @NotNull Customer customer){

// ...
}

@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers(){
return null;
}
}

Во-вторых, мы должны предоставить bean-компонент MethodValidationPostProcessor :

@Configuration
@ComponentScan({ "org.foreach.javaxval.methodvalidation.model" })
public class MethodValidationConfig {

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}

Контейнер теперь будет вызывать исключение javax.validation.ConstraintViolationException , если ограничение будет нарушено.

Если мы используем Spring Boot, контейнер зарегистрирует для нас bean-компонент MethodValidationPostProcessor , пока hibernate-validator находится в пути к классам.

3.2. Автоматическая проверка с помощью CDI (JSR-365)

Начиная с версии 1.1, Bean Validation работает с CDI (Contexts and Dependency Injection for Jakarta EE).

Если наше приложение работает в контейнере Jakarta EE, контейнер будет автоматически проверять ограничения метода во время вызова.

3.3. Программная проверка

Для ручной проверки метода в отдельном приложении Java мы можем использовать интерфейс javax.validation.executable.ExecutableValidator .

Мы можем получить экземпляр, используя следующий код:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();

ExecutableValidator предлагает четыре метода:

  • validateParameters() и validateReturnValue() для проверки метода
  • validateConstructorParameters() и validateConstructorReturnValue() для проверки конструктора

Проверка параметров нашего первого метода createReservation() будет выглядеть так:

ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
.getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set<ConstraintViolation<ReservationManagement>> violations
= executableValidator.validateParameters(object, method, parameterValues);

Примечание. Официальная документация не рекомендует вызывать этот интерфейс напрямую из кода приложения, а использовать его с помощью технологий перехвата методов, таких как АОП или прокси.

Если вам интересно, как использовать интерфейс ExecutableValidator , вы можете ознакомиться с официальной документацией .

4. Вывод

В этом руководстве мы кратко рассмотрели, как использовать ограничения метода с Hibernate Validator, а также обсудили некоторые новые функции JSR-380.

Во-первых, мы обсудили, как объявлять различные типы ограничений:

  • Ограничения одного параметра
  • Кросс-параметр
  • Ограничения возвращаемого значения

Мы также рассмотрели, как проверять ограничения вручную и автоматически с помощью Spring Validator.

Как всегда, полный исходный код примеров доступен на GitHub .