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

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

1. обзор

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

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

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

  • однопараметрические ограничения

  • кросс-параметр

  • возвратные ограничения

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

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

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

Для началаwe’ll first discuss how to declare constraints on method parameters and return values of methods.

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

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

Определить ограничения для отдельных параметров просто. We simply have to add annotations to each parameter as required:

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. Использование кросс-параметрических ограничений

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

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

Cross-parameter constraints can be considered as the method validation equivalent to class-level constraints. Мы могли бы использовать оба, чтобы реализовать проверку на основе нескольких свойств.

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

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

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

В отличие от однопараметрических ограниченийcross-parameter constraints are declared on the method or constructor:

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

    // ...
}

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

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

Для начала нам нужноdefine the constraint annotation:

@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[] payload() default {};
}

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

  • message – возвращает ключ по умолчанию для создания сообщений об ошибках, это позволяет нам использовать интерполяцию сообщений

  • groups - позволяет нам указать группы проверки для наших ограничений

  • payload - может использоваться клиентами Bean Validation API для назначения пользовательских объектов полезной нагрузки ограничению.

Подробнее о том, как определить пользовательское ограничение, см.the official documentation.

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

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
  implements ConstraintValidator {

    @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.. После этого мы проверяем, находятся ли оба в будущем иend послеbegin.

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

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

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

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

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

public class ReservationManagement {

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

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

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

  • Кроме того, список не должен содержатьnull записей.

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

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

public class ReservationManagement {

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

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

И сноваwe first have to define the constraint annotation:

@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[] payload() default {};
}

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

public class ValidReservationValidator
  implements ConstraintValidator {

    @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 какtarget в нашем интерфейсеValidReservation ранее,we can also annotate the constructor of Reservation to validate constructed instances:

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. Каскадная проверка

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

Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. Это работает как для параметров метода, так и для возвращаемых значений.

Предположим, у нас есть класс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 как на параметр метода,we can force the recursive validation of all properties:

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

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

  • По s-параметруreservation: запускает проверку s-объектаReservation, когда вызываетсяcreateNewCustomer()

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

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

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

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

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

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

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

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

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

Firstly, we have to annotate the beans, which shall be validated, with @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.example.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 (внедрение контекстов и зависимостей для Java EE).

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

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

Дляmanual method validation in a standalone Java application мы можем использовать интерфейс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> violations
  = executableValidator.validateParameters(object, method, parameterValues);

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

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

4. Заключение

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

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

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

  • Кросс-параметр

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

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

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