«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) {

    // ...
}

2.2. Использование ограничений между параметрами

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
}

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

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

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

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

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

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

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

2.3. Создание ограничений между параметрами

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

    // ...
}

Чтобы реализовать ограничение @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 {};
}

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

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

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

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

@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]);
    }
}

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

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

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

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

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

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

public class ReservationManagement {

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

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

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

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

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

public class ReservationManagement {

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

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

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

@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 {};
}

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

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);
    }
}

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

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

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
}

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

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

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

Класс Reservation может иметь свойство 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 в качестве параметра метода мы можем принудительно выполнить рекурсивную проверку всех свойств:

public class Reservation {

    @Valid
    private Customer customer;
	
    @Positive
    private int room;
	
    // further properties, constructor, getters and setters
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@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;
    }
}

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

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

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

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

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

Начиная с версии 1.1 проверка компонентов работает с CDI (внедрение контекстов и зависимостей для Jakarta EE).

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

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

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

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

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

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

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.

«