Статьи

Эффективная проверка доменной модели с помощью Hibernate Validator

Посмотрим правде в глаза, валидация модели предметной области всегда была довольно неукротимым чудовищем (и, скорее всего, это не изменится в ближайшее время), поскольку сам процесс валидации сильно привязан к контексту, в котором он происходит. Однако возможно инкапсулировать механизм валидации с помощью некоторых внешних библиотек классов, а не путать нашу жизнь, делая это самостоятельно с нуля. Именно здесь вступает в игру Hibernate Validator , эталонная реализация Java Beans Validation 1.0 / 1.1 (JSR 303), надежная модель проверки, основанная на аннотациях, которая поставляется с Java EE 6 и выше.

Эта статья прагматично расскажет вам, как использовать Hibernate Validator.

Контекстуальная природа валидации доменной модели

Независимо от того, какой язык вы выберете, построение богатой доменной модели является одной из самых сложных задач, которые вы можете решить как разработчик. И, кроме того, вам нужно убедиться, что данные, которые увлажняют модель, действительны, таким образом гарантируя, что их целостность должным образом поддерживается. К сожалению, во многих случаях сделать доменные объекты допустимыми в контексте содержащего приложения далеко не тривиально из-за внутренней контекстуальной природы самого процесса валидации.

Другими словами: если аргументы, принимаемые объектом домена, предоставляются средой приложения (например, с использованием простого внедрения зависимостей , фабрик , построителей и т. Д.), А не внешним верхним уровнем, то проверка объекта должна быть простым и ограниченным в очень ограниченном объеме.

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

В конце концов, все сводится к этому простому, но фундаментальному вопросу: что делает доменный объект действительным? Должно ли его состояние быть проверено до того, как оно будет сохранено или обновлено в базе данных или передано на другой уровень (уровни)? Логичный ответ: это зависит. Помните, что проверка всегда контекстная! Поэтому независимо от того, какой подход вы используете для определения допустимости ваших доменных объектов, проверка Java Beans сделает процесс более простым.

Представляем Java Bean Validation с JSR 303

До Java EE 6 в Java не было стандартного способа проверки полей класса домена с помощью централизованного механизма. Но с тех пор все изменилось к лучшему. Спецификация Java Beans Validation позволяет довольно легко выборочно проверять поля классов (и даже целые классы), используя ограничения, объявленные с несколькими интуитивно понятными аннотациями.

На момент написания этой статьи в JSR 303 было только две совместимые реализации, которые вы можете выбрать, Apache BVal и Hibernate Validator . Последняя является эталонной реализацией, которую можно использовать как отдельную библиотеку, полностью отделенную от популярной платформы ORM.

С учетом вышесказанного давайте теперь посмотрим, как начать использовать тандем Java Beans Validation / Hibernate Validator для эффективной проверки модели предметной области в реальном мире.

Определение базовой доменной модели

Как обычно, хороший способ показать, как использовать проверку Java Beans для проверки модели предметной области, – на конкретном примере. Учитывая, что стандарт реализует схему проверки на основе аннотаций, классы, являющиеся частью модели предметной области, всегда должны быть ограничены аннотациями.

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

public class User {

    private int id;

    @NotEmpty(message = "Name is mandatory")
    @Size(min = 2, max = 32,
            message = "Name must be between 2 and 32 characters long")
    private String name;

    @NotEmpty(message = "Email is mandatory")
    @Email(message = "Email must be a well-formed address")
    private String email;

    @NotEmpty(message = "Biography is mandatory")
    @Size(min = 10, max = 140,
            message = "Biography must be between 10 and 140 characters long")
    private String biography;

    public User(String name, String email, String biography) {
        this.name = name;
        this.email = email;
        this.biography = biography;
    }

    // Setters and Getters for name, email and biography

}

Не стоит обсуждать ничего, кроме ограничений, объявленных в верхней части каждого поля. Например, @NotEmpty(message = "Name is mandatory")name Несмотря на то, что это довольно очевидно, атрибут message Более того, можно настроить множество ограничений с параметрами, чтобы выразить более точные критерии. Рассмотрим @Size(min = 2, max = 32, message = "...") Это хорошо отражает именно то, что говорится в сообщении. Не ракетостроение, верно?

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

Проверка гибернации микроскопа

Проверка ограничений

На данный момент нам удалось определить класс модели с ограничениями с помощью Java Beans Validation, что вполне нормально. Но вам может быть интересно, каковы практические преимущества этого? Лаконичный ответ: нет – пока. Конечно, класс готов к проверке, но отсутствующий фрагмент здесь имеет механизм, способный сканировать аннотации, проверять значения, присвоенные ограниченным полям, и возвращать ошибки проверки (или в терминологии JSR 303 нарушения ограничения). ). И именно так Hibernate Validator работает под капотом.

Но давайте будем откровенны: вышеприведенное объяснение будет просто техническим бредом, если мы не увидим хотя бы надуманного примера, показывающего, как использовать Hibernate Validator для проверки класса User

 Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
User user = new User("John Doe", "no-mail", "");
validator
    .validate(user).stream()
    .forEach(violation -> System.out.println(violation.getMessage()));

Это было действительно легко, не так ли? Фрагмент сначала получает экземпляр Hibernate Validator с помощью статических buildDefaultValidatorFactory()getValidator()validate() В этом случае нарушения ограничений отображаются в консоли с использованием потоков и лямбд в целом. Однако возможно получить тот же результат, используя стандартный цикл forforEach

Инкапсуляция логики проверки в компоненте службы

Конечно, смехотворно просто (и очень, очень откровенно говоря) начать отбрасывать экземпляры Validator Но это было бы вопиющим нарушением принципа СУХОЙ (то есть WET-решение), которое привело бы к дублированию кода на нескольких уровнях. Да, безусловно, плохой дизайн.

Вместо этого было бы намного эффективнее инкапсулировать Hibernate Validator внутри границ разделенного сервисного компонента, который потенциально может быть использован везде. Короче говоря, все, что нам нужно сделать, чтобы обернуть его за пределы такого сервисного компонента, – это простой контракт, определенный через интерфейс, и широко общая реализация:

 public interface EntityValidator<T> {

   Set<ConstraintViolation<T>> validate(T t);

}

public class BaseEntityValidator<T> implements EntityValidator<T> {

   protected final Validator validator;

   public BaseEntityValidator(Validator validator) {
       this.validator = validator;
   }

   public Set<ConstraintViolation<T>> validate(T t) {
       return validator.validate(t);
   }
}

Используя простое делегирование , мы создали работающий модуль проверки, который принимает JSR 303-совместимую реализацию в конструкторе и использует его метод validate()T Единственная деталь, на которую стоит обратить внимание, это то, что рассматриваемый метод возвращает набор, содержащий соответствующие объекты нарушения ограничений, после того, как объект был должным образом проверен.

На первый взгляд класс BaseEntityValidator Но это только вводящее в заблуждение впечатление, поверь мне. Класс не только использует преимущества основанного на интерфейсе полиморфизма (он же полиморфизм подтипов ), он также действует как адаптер для самого валидатора, что позволяет выборочно предоставлять собственные методы валидатора клиентскому коду и даже добавлять специфичные для домена методы.

Если вам интересно, как использовать класс BaseEntityValidator

 // ideally the validator is injected but in this case we’ll create it explicitly
EntityValidator<User> userValidator =  new BaseEntityValidator<>(
        Validation.buildDefaultValidatorFactory().getValidator());
User user = new User("John Doe", "no-email", "");
validator
    .validate(user).stream()
    .forEach(violation -> System.out.println(violation.getMessage()));

Кроме того, есть много места для использования класса в разных контекстах и ​​сценариях. Например, мы могли бы разработать базовое веб-приложение, аналогичное тому, которое я создал в учебном пособии по сервлету API , и эффективно и продуктивно проверять пользовательские данные, переданные через форму HTML. Поскольку создание такого веб-приложения определенно выходит за рамки этого поста, задача останется для вас домашней работой на тот случай, если вы захотите начать тянуть поводья Hibernate Validator в веб-ландшафте.

Создание пользовательских ограничений проверки и валидаторов

На этом этапе должно быть ясно, что Hibernate Validator является непревзойденным конкурентом, когда дело доходит до проверки доменных объектов простым способом. Но это еще не все: даже когда JSR 303 поставляется по умолчанию с широким набором нарушений ограничений, которые будут покрывать наиболее типичные требования валидации ежедневных разработчиков, таких как вы и я, стоит упомянуть, что его базовая функциональность может быть легко расширена с помощью пользовательские ограничения проверки и валидаторы.

Фактически, создание настраиваемого ограничения валидации и соответствующего настраиваемого валидатора – это простой процесс, который сводится к следующему:

  1. Определение интерфейса аннотации, который должен указывать цель (и) ограничения проверки, как долго информация аннотации будет доступна компилятору (также известная как политика хранения ), и, наконец, пользовательский класс проверки, связанный с ограничением.
  2. Создание собственного класса валидации.

Как обычно, практический пример – лучший способ понять основную логику этого процесса. Итак, допустим, мы хотим применить пользовательское ограничение проверки электронной почты к классу User

 @Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
public @interface ValidEmail {

    String message()  default "Email must be a well-formed address";

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

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

Как показано выше, аннотация ValidEmail@Retention(RetentionPolicy.RUNTIME)(@Target({ElementType.METHOD, ElementType.FIELD})@ConstraintEmailValidator

Наконец, метод message()

Кроме того, метод groups()каждой группы . По умолчанию все ограничения проверяются в группе по умолчанию, что означает, что каждое ограничение будет проверяться независимо от фазы жизненного цикла целевого объекта. Однако можно создать разные группы в форме простых интерфейсов и указать, какие ограничения должны быть проверены в соответствии с определенной фазой жизненного цикла. Для краткости в приведенном выше примере используется группа по умолчанию.

Наконец, метод payload()объектов полезной нагрузки , которые содержат дополнительную информацию об ограничениях и могут быть получены при проверке целевого объекта.

Теперь давайте обратимся к классу EmailValidator Вот как может выглядеть наивная реализация:

 public class EmailValidator implements ConstraintValidator<ValidEmail, String> {

    private static final Pattern VALID_EMAIL_PATTERN = Pattern.compile(
            "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$",
            Pattern.CASE_INSENSITIVE);

    @Override
    public void initialize(ValidEmail constraintAnnotation) {
        // can be used to set the instance up for validation
    }

    @Override
    public boolean isValid(
            String email, ConstraintValidatorContext constraintValidatorContext) {
        Matcher matcher = VALID_EMAIL_PATTERN.matcher(email);
        return matcher.find();
    }

}

Здесь стоит выделить только пару деталей: во-первых, пользовательские валидаторы должны реализовывать собственный интерфейс ConstraintValidatorValidEmailString Вторая деталь – это метод isValid()

Наконец, что не менее важно, объявление поля emailUserEmailValidator

 @ValidEmail
private String email;

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

Это было довольно легко понять, верно? Конечно, я не говорю, что вам всегда нужно будет испортить свою жизнь, создавая собственные ограничения и валидаторы, так как это снизит функциональность проверки Java Beans до нуля. Тем не менее, с точки зрения дизайна полезно знать, что стандарт предлагает достойный уровень настройки.

Выводы

К этому моменту вы узнали основы того, как использовать Java Beans Validation и Hibernate Validator бок о бок для простой проверки ваших доменных объектов. Кроме того, вы увидели, что действительно легко расширить основные функциональные возможности спецификации с помощью пользовательских ограничений и пользовательских валидаторов, в тех случаях, когда стандартные настройки просто не соответствуют вашим личным потребностям. Кроме того, имейте в виду, что JSR 303 находится в стадии разработки и движется довольно быстрыми темпами (на самом деле выпуск Java Beans Validation 2.0 не за горами), поэтому будьте в курсе последних новостей .

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

Вполне возможно, что наибольшее преимущество использования спецификации заключается в том, что она позволяет легко внедрять компоненты проверки с высокой степенью разделения, которые можно использовать буквально везде. Если этот единственный аргумент недостаточно убедителен для того, чтобы вы сразу начали использовать Java Beans Validation, конечно, ничего не будет. Чего же ты ждешь?