Валидация методов — Jakarta Bean Validation
В этой статье, посвящённой Jakarta Bean Validation, будет подробно рассмотрен процесс валидации методов, а также применение ограничений к аргументам и возвращаемым значениям методов и конструкторов. Кроме этого будет рассмотрено каскадирование валидации, а так же нюансы валидации методов при использовании наследования.
Цикл статей о Jakarta Bean Validation
Данная статья является частью цикла статей о Jakarta Bean Validation:
- Знакомство с Jakarta Bean Validation 3.0
- Валидация объектов
- Валидация методов (Вы сейчас её читаете)
- Группы валидации (В процессе написания)
- Создание ограничения и валидатора (В процессе написания)
- Интерполяция сообщений (В процессе написания)
- Конфигурация валидатора (В процессе написания)
Валидация методов
Кроме валидации объектов при использовании Jakarta Bean Validation доступна валидация аргументов и возвращаемых значений методов и конструкторов.
Валидация методов возможна при помощи интерфейса ExecutableValidator, получить экземпляр которого можно при помощи метода forExecutables() интерфейса Validator, как это показано в следующем примере кода:
Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); ExecutableValidator executableValidator = validator.forExecutables();
Валидация аргументов методов
Ограничения могут применяться к аргументам методов, в этом случае аннотацией ограничения отмечается валидируемый аргумент:
class Candidate {
int age;
// Ограничение аргумента конструктора
Candidate(@Min(18) int age) {
this.age = age;
}
// Ограничение аргумента метода
void setAge(@Min(18) int age) {
this.age = age;
}
}В XML эти ограничения задаются при помощи <parameter> следующим образом:
<bean class="Candidate">
<!-- Ограничение аргумента конструктора -->
<constructor>
<parameter type="int">
<constraint annotation="Min">
<element name="value">18</element>
</constraint>
</parameter>
</constructor>
<!-- Ограничение аргумента метода -->
<method name="setAge">
<parameter type="int">
<constraint annotation="Min">
<element name="value">18</element>
</constraint>
</parameter>
</method>
</bean>Провести валидацию аргументов метода setAge можно при помощи метода validateParameters, который принимает следующие аргументы:
- Валидируемый объект
- Валидируемый метод
- Массив с аргументами метода
- Список групп валидации
В коде это будет выглядеть следующим образом:
class ValidationTest {
ExecutableValidator executableValidator;
@Test
void test() throws NoSuchMethodException {
// 1. Создать тестируемый объект
var candidate = new Candidate();
// 2. Получить тестируемый метод
var method = candidate.getClass().getMethod("setAge", int.class);
// 3. Провалидировать аргументы метода
var violations = this.executableValidator
.validateParameters(candidate, method, new Object[]{17});
// 4. Проверить, что результат содержит 1 ошибку валидации
assertEquals(1, violations.size());
}
}Как видно из примера кода, для валидации аргументов метода требуется получить валидируемый метод при помощи API рефлексии.
Валидация аргумента конструктора выглядит схожим образом, но с использованием метода validateConstructorParameters, который принимает следующие аргументы:
- Валидируемый конструктор
- Массив с аргументами конструктора
- Список групп валидации
class ValidationTest {
ExecutableValidator executableValidator;
@Test
void test() throws NoSuchMethodException {
// 1. Получить конструктор
var constructor = Candidate.class.getConstructor(int.class);
// 2. Провалидировать аргументы конструктора
var violations = this.executableValidator
.validateConstructorParameters(constructor, new Object[]{17});
// 3. Проверить, что результат содержит 1 ошибку валидации
assertEquals(1, violations.size());
}
}Перекрёстные ограничения
При работе с методами могут возникать ситуации, когда в рамках одной проверки требуется проверить значения нескольких аргументов одновременно. В таких ситуациях может быть полезным перекрёстное ограничение, которое применяется сразу к нескольким аргументам метода. Аннотация ограничения в этом случае применяется к самому методу, а не к аргументам:
class Candidate {
@MinAges(maleMinAge = 18, femaleMinAge = 20)
Candidate(int age, Gender gender) {
}
@MinAges(maleMinAge = 18, femaleMinAge = 20)
void setAgeAndGender(int age, Gender gender) {
}
}Jakarta Bean Validation не предоставляет стандартных перекрёстных ограничений, поэтому я использовал своё ограничение для демонстрации:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = MinAgesValidator.class)
public @interface MinAges {
String message() default "akosarev.constraints.MinAges.message";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int maleMinAge();
int femaleMinAge();
}Про создание и использование собственных ограничений будет рассказано в следующей статье.
В XML перекрёстные ограничения указываются при помощи <cross-parameter> следующим образом:
<bean class="Candidate">
<!-- Перекрёстное ограничение для аргументов конструктора -->
<constructor>
<cross-parameter>
<constraint annotation="MinAges">
<element name="maleMinAge">18</element>
<element name="femaleMinAge">20</element>
</constraint>
</cross-parameter>
</constructor>
<!-- Перекрёстное ограничение для аргументов метода -->
<method name="setAge">
<cross-parameter>
<constraint annotation="MinAges">
<element name="maleMinAge">18</element>
<element name="femaleMinAge">20</element>
</constraint>
</cross-parameter>
</method>
</bean>Валидация возвращаемого значения метода
В случае с методами и конструкторами ограничения также применимы к возвращаемым значениям. Аннотация ограничения в этом случае также применяется к методу:
class CandidatesGroup {
// Ограничение возвращаемого значения конструктора
@ValidCandidatesGroup
CandidatesGroup() {
}
// Ограничение возвращаемого значения метода
@Min(0)
int countMaleCandidates() {
//
}
}В случае с конструктором будет провалидирован созданный им экземпляр класса.
В XML ограничения возвращаемых значений задаются при помощи <return-value>:
<bean class="CandidatesGroup">
<constructor>
<return-value>
<constraint annotation="ValidCandidatesGroup"/>
</return-value>
</constructor>
<method name="countMaleCandidates">
<return-value>
<constraint annotation="Min">
<element name="value">0</element>
</constraint>
</return-value>
</method>
</bean>При работе с методами можно проводить и валидацию возвращаемых значений при помощи метода validateReturnValue, который принимает следующие аргументы:
- Валидируемый объект
- Валидируемый метод
- Валидируемое значение, возвращённое методом
- Список групп валидации
Выглядит валидация возвращаемого значения метода следующим образом:
class ValidationTest {
ExecutableValidator executableValidator;
@Test
void test() throws NoSuchMethodException {
// 1. Получить конструктор
var constructor = Candidate.class.getConstructor(int.class);
// 2. Провалидировать аргументы
var violations = this.executableValidator
.validateConstructorParameters(constructor,
new Object[]{17});
// 3. Проверить, что результат содержит 1 ошибку валидации
assertEquals(1, violations.size());
}
}Конструкторы, как известно, возвращают созданные ими экземпляры классов. Результаты выполнения конструкторов тоже можно проверить. Его валидация будет выглядеть следующим образом:
class ValidationTest {
ExecutableValidator executableValidator;
@Test
void test() throws NoSuchMethodException {
// 1. Получить конструктор
var constructor = Candidate.class.getConstructor(int.class,
Candidate.Gender.class);
// 2. Создать новый экземпляр Candidate
var candidate = new Candidate(17, Candidate.Gender.MALE);
// 3. Провалидировать экземпляр Candidate
var violations = this.executableValidator
.validateConstructorReturnValue(constructor, candidate);
// 4. Проверить, что результат содержит 1 ошибку валидации
assertEquals(1, violations.size());
}
}Каскадирование валидации методов
При необходимости есть возможность использования каскадирования валидации методов, так же как и в случае с валидацией объектов. Если требуется провалидировать аргумент или возвращаемое значение метода или конструктора, то необходимо нужный элемент отметить аннотацией @Valid:
class CandidatesGroup {
// Каскадирование валидации созданного
// экземпляра CandidatesGroup
@Valid
CandidatesGroup() {
}
// Каскадирование валидации возвращаемого значения
@Valid
Candidate getBestCandidate() {
// Какая-то логика
}
// Каскадирование валидации аргумента
void addCandidate(@Valid Candidate candidate) {
}
}Того же эффекта можно добиться в XML при помощи <valid/>:
<?xml version="1.0" encoding="UTF-8" ?>
<constraint-mappings
xmlns="https://jakarta.ee/xml/ns/validation/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/mapping
https://jakarta.ee/xml/ns/validation/validation-mapping-3.0.xsd"
version="3.0">
<bean class="pro.akosarev.sandbox.CandidatesGroup" ignore-annotations="true">
<constructor>
<return-value>
<valid/>
</return-value>
</constructor>
<method name="getBestCandidate">
<return-value>
<valid/>
</return-value>
</method>
<method name="addCandidate">
<parameter type="pro.akosarev.sandbox.Candidate">
<valid/>
</parameter>
</method>
</bean>
</constraint-mappings>Ограничения методов при наследовании
При использовании ограничений методов следует помнить о двух правилах:
- Предусловия не могут быть усилены в классах-наследниках
- Постусловия не могут быть ослаблены в классах-наследниках
Эти ограничения восходят к принципу подстановки Барбары Лисков (Liskov Substitution Principle; LSP) из списка принципов SOLID, про который я рассказывал ранее в одной из статей.
Jakarta Bean Validation строго следует этим правилам, и соблюдает первое из них запрещая применение ограничений к аргументам методов дочерних классов, если к аргументам метода родительского класса или интерфейса уже применены ограничения.
Допустим, есть интерфейс ParentCandidatesGroup, который объявляет метод addCandidate с ограничениями аргументов. Тогда объявление любых ограничений к этому методу в классе-наследнике будет приводить к ошибке даже в том случае, если ограничения применяются к другим аргументам, так как это в целом является усилением предусловий:
interface ParentCandidatesGroup {
void addCandidate(@Min(18) int age, Gender gender);
}
class CandidatesGroup implements ParentCandidatesGroup {
@Override
public void addCandidate(int age, @NotNull Gender gender) {
// какая-то логика
}
}Пример такой ошибки:
jakarta.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method CandidatesGroup#addCandidate(int, Gender) redefines the configuration of ParentCandidatesGroup#addCandidate(int, Gender).
Кроме этого, если класс расширяет или реализует два независимых класса или интерфейса, объявляющих метод с одинаковой сигнатурой, то к такому методу ограничения могут быть применены только в реализующем классе, но не в родительских классах:
interface CandidateArchive {
void addCandidate(int age, Gender gender);
}
interface ParentCandidatesGroup {
void addCandidate(int age, Gender gender);
}
class CandidatesGroup implements ParentCandidatesGroup, CandidateArchive {
@Override
public void addCandidate(@Min(18) int age, @NotNull Gender gender) {
// какая-то логика
}
}В противном случае попытка валидации завершится следующей ошибкой:
jakarta.validation.ConstraintDeclarationException: HV000152: Two methods defined in parallel types must not declare parameter constraints, if they are overridden by the same method, but methods ParentCandidatesGroup#addCandidate(int, Gender) and CandidateArchive#addCandidate(int, Gender) both define parameter constraints.
В случае с возвращаемыми значениями методов всё работает с точностью до наоборот: ограничения для них могут быть указаны в родительских типах, а так же в дочерних, так как это не является ослаблением постусловий, и при валидации все они будут проверяться.
interface ParentCandidatesGroup {
@NotNull
Candidate getBestCandidate();
}
class CandidatesGroup {
@ValidCandidate(value = 18, gender = Gender.MALE)
public Candidate getBestCandidate() {
// Какая-то логика
}
}