Валидация методов — Jakarta Bean Validation

Валидация методов — Jakarta Bean Validation, image #1

В этой статье, посвящённой 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() {
        // Какая-то логика
    }
}

Полезные ссылки

93 views·3 shares