Статьи

Использование проверки Java Bean для параметров метода и возвращаемых значений

Система статических типов Java — это надежный механизм, который позволяет вам задавать предварительные условия и постусловия метода (с точки зрения вызывающего: что предоставлять и чего ожидать) с помощью компилятора, гарантирующего выполнение этих условий во время выполнения . Java Bean Validation продвигает эту модель ограничений ближе и ближе к конкретному домену. Он предоставляет полноценный API проверки, который не только позволяет проверять поля класса с помощью набора интуитивных ограничений проверки. Как мы увидим в этом посте, он также дает разработчикам возможность повторно использовать эти ограничения в аргументах и ​​возвращать значения как в обычных методах, так и в конструкторах, а также проверять их способом конструирования по контракту — вручную или автоматически с помощью перехватчиков Java EE.

Если вы хотите взглянуть на основные функции стандарта, ознакомьтесь с этой вводной статьей . Описанные там валидации используют API-интерфейс Validator то время как в этой статье здесь приводятся методы, предоставляемые более новым интерфейсом ExecutableValidator — оба являются частью стандартных реализаций, таких как Hibernate Validator и Apache BVal .

Валидация аргументов метода

Первый метод, который мы рассмотрим в этом обзоре, это validateParameters() , который проверяет аргументы данного метода. Рассмотрим пример ниже, который связывает некоторые основные ограничения с установщиками следующего класса User :

 public class User { private String name; private String email; public User(){} public void setName( @NotEmpty(message = "Name may not be empty") String name) { this.name = name; } public void setEmail( @Email(message = "Email must be a well-formed email address") String email) { this.email = email; } // getters for name and email } 

Действительно легко отследить контракт, согласованный целевым классом с вызывающей стороной, без необходимости сканировать класс сверху вниз и более глубоко взглянуть на реализации методов. Проще говоря, предварительным условием setName() является непустая строка, в то время как предварительным условием setEmail() является правильно сформированный адрес электронной почты.

Контракт, конечно, должен быть утвержден в какой-то момент. Для этого в стандарте предусмотрен вышеупомянутый метод validateParameters() :

 ExecutableValidator executableValidator = Validation .buildDefaultValidatorFactory() .getValidator() .forExecutables(); User user = new User(); try { Method setName = user.getClass() .getMethod("setName", String.class); executableValidator .validateParameters(user, setName, new Object[]{""}).stream() .map(ConstraintViolation::getMessage) .forEach(System.out::println); } catch (NoSuchMethodException e) { e.printStackTrace(); } 

Прежде всего следует подчеркнуть, что все методы, которые выполняют проверку параметров метода и возвращаемых значений (это относится и к конструкторам), являются частью API ExecutableValidator , а не классическим Validator . Второй — это механика самого процесса валидации: после того, как был создан реализатор ExecutableValidator , весь процесс валидации сводится к вызову правильного метода валидации с необходимыми аргументами.

В приведенном выше примере предварительные условия setName() проверяются с помощью метода validateParameters() , который принимает экземпляр целевого класса, проверяемый метод и аргументы, хранящиеся в массиве объектов . Учитывая, что в этом случае массив содержит только пустую строку, предварительное условие setName() не выполняется, что вызывает нарушение ограничения.

Как насчет проверки предусловия setEmail() вместо этого?

 try { Method setEmail = user.getClass() .getMethod("setEmail", String.class); executableValidator .validateParameters(user, setEmail, new Object[]{""}).stream() .map(ConstraintViolation::getMessage) .forEach(System.out::println); } catch (NoSuchMethodException e) { e.printStackTrace(); } 

Это довольно просто понять.

Проверка правильности возвращаемых значений метода

В той же строке можно указать постусловие метода и проверить его с помощью метода validateReturnValue() . Чтобы продемонстрировать, как легко проверить возвращаемое значение метода, давайте проведем рефакторинг более раннего класса User , например:

 public class User { private String name; private String email; public User(){} // setter and getter for name, setter for email @Email(message = "Email must be a well-formed email address") public String getEmail() { return email; } } 

В этом случае постусловие метода getEmail() — это правильно сформированный адрес электронной почты. Вот как проверить постусловие метода с помощью метода validateReturnValue() :

 User user = new User(); try { Method getEmail = user.getClass().getMethod("getEmail"); executableValidator .validateReturnValue(user, getEmail, "no-email").stream() .map(ConstraintViolation::getMessage) .forEach(System.out::println); } catch (NoSuchMethodException e) { e.printStackTrace(); } 

Как и ожидалось, это также вызовет нарушение ограничения, так как постусловие метода getEmail() не выполняется строкой "no-email" , которая передается validateReturnValue() .

Проверка аргументов конструктора

Как и методы, стандарт позволяет проверять аргументы в конструкторах с помощью метода validateConstructorParameters() . Он делает именно то, что обещает за чистую монету: да, он проверяет параметры конструктора! Это еще один явный признак хорошо разработанного API.

Чтобы использовать данный метод, класс User должен быть реорганизован следующим образом:

 public class User { private String name; private String email; public User(){} public User( @NotEmpty(message = "Name may not be empty") String name, @Email(message = "Email must be a well-formed email address") String email) { this.name = name; this.email = email; } // setters and getters for name and email } 

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

 User user = new User("", ""); try { Constructor constructor = user.getClass() .getConstructor(new Class[] {String.class, String.class}); executableValidator .validateConstructorParameters(constructor, new Object[]{"", ""}) .stream() .map(ConstraintViolation::getMessage) .forEach(System.out::println); } catch (NoSuchMethodException e) { e.printStackTrace(); } 

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

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

«Используйте Java Bean Validation для проверки вызовов методов»

Автоматическая проверка с помощью перехватчиков Java EE

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

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

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

Посмотрите на следующий класс домена:

 public class User { private String name; private String email; public User(){} @Interceptors(MethodInterceptor.class) public void setName(@NotEmpty(message = "Name may not be empty") String name) { this.name = name; } public String getName() { return name; } public void setEmail(String email) { this.email = email; } @Interceptors(MethodInterceptor.class) @Email(message = "Email must be a well-formed address") public String getEmail() { return email; } } 

Как показано выше, класс User теперь имеет ограниченный установщик и ограниченный получатель. Единственная деталь, на самом деле заслуживающая внимания, заключается в том, что ограниченные методы связаны с перехватчиком @Interceptors аннотации @Interceptors , за которой следует класс перехватчика, заключенный в скобки. Аннотация связывает ограниченные методы с перехватчиком, что означает, что всякий раз, когда методы вызываются, вызовы могут быть перехвачены, и ограничения могут быть проверены, следуя определенной стратегии.

Вот как может быть реализован типичный метод-перехватчик:

 @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface MethodInterceptorBinding { } @Interceptor @MethodInterceptorBinding public class MethodInterceptor { @Inject private Validator validator; @AroundInvoke public Object validateMethodInvocation(InvocationContext ctx) throws Exception { Set<ConstraintViolation<Object>> violations; ExecutableValidator executableValidator = validator.forExecutables(); violations = executableValidator.validateParameters( ctx.getTarget(), ctx.getMethod(), ctx.getParameters()); processViolations(violations); Object result = ctx.proceed(); violations = executableValidator.validateReturnValue( ctx.getTarget(), ctx.getMethod(), result); processViolations(violations); return result; } private void processViolations(Set<ConstraintViolation<Object>> violations) { violations.stream() .map(ConstraintViolation::getMessage) .forEach(System.out::println); } } 

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

  1. Аннотация @InterceptorBinding : требуется для привязки метаданных, таких как RetentionPolicy и соответствующие цели , к пользовательской аннотации перехватчика @MethodInterceptorBinding .
  2. Класс перехватчика: обычный класс с аннотациями @Interceptor и @MethodInterceptorBinding , помечающий его как перехватчик.
  3. Аннотация @AroundInvoke : используется для определения перехватчика, который перехватывает вызовы методов, к которым он привязан (возможно создать еще несколько типов перехватчиков, но они для простоты здесь опущены). В этом случае метод validateMethodInvocation() вставляет между вызовами ограниченные методы класса User и вызывающей стороны.
  4. Реализация перехватчика: объект Validator внедряется в класс перехватчика с помощью CDI , который используется для получения экземпляра ExcecutableValidator и проверки ограничений в аргументах метода и возвращаемых значениях.
  5. Вывод перехватчика: когда перехватчик проверяет ограниченные методы, потенциальные нарушения ограничений выводятся на консоль с помощью processViolations() .

Что касается processViolations() на консоли, официальная документация настоятельно рекомендует создать ConstraintViolationException , заключающее в себе нарушения изнутри перехватчика, когда метод не проходит проверку, что имеет большой смысл, поскольку это обеспечивает следующее:

  1. Поток управления достигает тела метода, только если клиентский код выполнил предварительные условия метода.

  2. Поток управления возвращается к клиентскому коду, если условия публикации метода гарантированы.

Я не делал этого здесь, чтобы сделать пример немного легче для понимания.

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

 public class Application { @Inject private User user; public void execute() { user.setName("John"); user.setEmail("no-email"); user.getEmail(); } } 

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

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

Резюме

В этом посте мы узнали, как использовать методы, являющиеся частью интерфейса ExecutableValidator , для проверки аргументов и возвращаемых значений как в обычных методах, так и в конструкторах. Кроме того, мы увидели, что можно интегрировать стандарт с перехватчиками CDI и Java EE и запустить автоматическую проверку методов.

Сначала процесс интеграции выглядит довольно сложным, но усилия по внедрению дополнительного уровня перехватчиков в значительной степени компенсируются преимуществами инкапсуляции логики проверки в одном месте и реализации инверсии управления . И последнее, но не менее важное: имейте в виду, что функциональность Bean Validation не ограничивается только проверкой аргументов метода и возвращаемых значений, поскольку предлагает множество других полезных функций, таких как проверка отдельных полей классов и графов объектов с помощью одного метода. вызов. Итак, если вам интересно узнать, как делать все эти вещи, не поднимаясь по крутой кривой обучения, вы можете проверить эти более короткие посты, чтобы узнать, как проверять отдельные свойства и поля и как использовать @Valid для проверки целых графов объектов. ,