Создание пользовательской аннотации проверки с нуля в Spring MVC!

1. Введение

В этой статье мы рассмотрим, как разработать собственный валидатор в Spring MVC. Этот валидатор будет реализован в виде аннотации, аналогично ограничениям проверки, предоставляемым Bean Validation API, которые мы обсуждали в нашей предыдущей статье Spring MVC Part3.

Чтобы углубиться в эту тему, необходимо иметь базовое представление об аннотациях в Java. Если вам интересно, рекомендую прочитать книгу Core Java: Advanced Features, Volume 2 by Cay S. Horstmann.

2. Анализ ограничения проверки @Size

Чтобы глубже понять ограничения проверки в Spring MVC, давайте начнем с анализа одной конкретной реализации: предварительно созданного ограничения @Size. Это ограничение позволяет нам проверять размер поля или свойства.

Чтобы изучить его реализацию, давайте взглянем на исходный код валидатора @Size.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(
    validatedBy = {}
)
public @interface Size {
    String message() default "{javax.validation.constraints.Size.message}";

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

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

    int min() default 0;

    int max() default Integer.MAX_VALUE;

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Size[] value();
    }
}

Давайте разберем аннотацию построчно и поработаем над созданием собственной пользовательской аннотации:

@интерфейс ключевое слово

Аннотация определяется с помощью ключевого слова @interface, за которым следует имя аннотации. Например:

@interface MyAnnotation {
}

Аннотация может иметь элементы, подобные классу, которые называются элементами аннотации. Эти элементы определяются методами внутри аннотации. Например, следующее определяет аннотацию с одним членом, называемым «значение»:

@interface MyAnnotation {
  int value();
}

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

Вы также можете аннотировать аннотацию другими аннотациями, это называется мета-аннотацией.

@Цель Аннотация

Аннотация @Target используется в аннотациях Java для указания элементов, к которым может применяться аннотация. Это позволяет нам определять целевые типы, такие как классы, поля, методы и т. д., где можно использовать аннотацию. Используя аннотацию @Target, мы можем ограничить использование нашей аннотации определенными элементами, гарантируя, что она применяется только там, где это имеет смысл, и обеспечивая больший контроль над ее использованием.

Например, если мы создаем пользовательскую аннотацию, предназначенную для применения только к полям, мы можем использовать @Target(ElementType.FIELD), чтобы применить это ограничение. Таким образом, если кто-то попытается применить нашу аннотацию к методу или классу, это приведет к ошибке компиляции.

Аннотация @Target принимает один аргумент, который представляет собой перечисление ElementType констант, таких как:

  • ElementType.ANNOTATION_TYPE : может использоваться для типа аннотации.
  • ElementType.CONSTRUCTOR: может использоваться в конструкторе.
  • ElementType.FIELD : может использоваться в поле или свойстве.
  • ElementType.METHOD: может использоваться в методе.

В нашем примере @Size снабжен аннотацией

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})

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

@RetentionАннотация

Аннотация @Retention определяет срок службы аннотации. Он используется для указания, когда аннотация должна быть доступна для JVM. Есть три возможных значения для RetentionPolicy:

  • ИСТОЧНИК: аннотация будет сохранена только в исходном коде и будет удалена компилятором. Он недоступен во время выполнения.
  • CLASS: аннотация будет сохранена компилятором и включена в файл класса, но не будет доступна во время выполнения. Это политика хранения по умолчанию.
  • ВРЕМЯ ВЫПОЛНЕНИЯ: аннотация будет сохранена компилятором и включена в файл класса, и она будет доступна во время выполнения через API Reflection.

В нашем примере @Size снабжен аннотацией

@Retention(RetentionPolicy.RUNTIME)

это означает, что эта аннотация будет доступна во время выполнения через Reflection API.

@Ограничение

При использовании аннотации @Constraint мы должны предоставить класс реализации, который определяет логику проверки для пользовательского ограничения. Этот класс реализации должен реализовать интерфейс ConstraintValidator, предоставляемый Bean Validation API.

Для аннотации @Constraint требуется атрибут validatedBy. Этот параметр указывает класс, который реализует интерфейс ConstraintValidator и обеспечивает логику проверки для пользовательского ограничения.

Например, предположим, что у нас есть пользовательское ограничение с именем @MyConstraint, и у вас есть класс реализации с именем MyConstraintValidator, который реализует ConstraintValidator<MyConstraint, Object>. Вы бы указали это следующим образом:

@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
    // ...
}

Важно отметить, что для интерфейса ConstraintValidator требуются два параметра типа: пользовательский тип аннотации ограничения (MyConstraint в примере) и тип проверяемого элемента (Object в примере). Эти параметры типа определяют связь между ограничением и его средством проверки.

3. Внедрение пользовательской проверки: создание ограничения @MultipleOfFive шаг за шагом

Чтобы следовать за мной, начните с создания проекта Spring Boot или скачайте исходный код, связанный с этой статьей, с GitHub.

Давайте начнем с разработки аннотации. Мы создадим простую аннотацию @MultipleOfFive. Эта аннотация будет использоваться для проверки того, является ли целое число кратным пяти или нет.

  • Аннотация @MultipleOfFive
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MultipleOfFiveValidator.class )
public @interface MultipleOfFive {
    String message() default "The number provided is not a multiple of 5. Please try again.";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}
  • Класс MultipleOfFiveValidator

Как упоминалось ранее в этой статье, логика проверки должна быть реализована в классе, который реализует интерфейс ConstraintValidator, предоставляемый API проверки компонентов.

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;


public class MultipleOfFiveValidator implements ConstraintValidator<MultipleOfFive , Integer>{
    @Override
    public void initialize(MultipleOfFive constraintAnnotation) {
    }

    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return integer %5 == 0;
    }
}

В этом классе MultipleOfFiveValidator:

  • Мы реализуем интерфейс ConstraintValidator<MultipleOfFive, Integer>, где MultipleOfFive относится к нашей пользовательской аннотации, а Integer представляет тип проверяемого поля (в данном случае целое число).
  • Метод initialize вызывается во время инициализации валидатора, но, поскольку нам не нужна никакая пользовательская инициализация, он оставлен пустым.
  • Метод isValid содержит логику проверки. Он проверяет, делится ли предоставленное значение на 5 (т. е. кратно ли 5), и возвращает true, если оно допустимо, или false в противном случае.

4. Демо

В демонстрационных целях я создал простой REST API вместе с DTO (объектом передачи данных), который мы будем проверять.

  • Запрос сотрудника
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeRequest {

    private String name ;

    @MultipleOfFive
    private int age ;

}
  • Контроллер сотрудников
@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    @PostMapping("")
    public ResponseEntity<String> saveEmployee(@Valid @RequestBody EmployeeRequest employeeRequest, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            List<String> errorMessages = bindingResult.getAllErrors().stream()
                    .map(ObjectError::getDefaultMessage)
                    .collect(Collectors.toList());

            return ResponseEntity.badRequest().body(String.join(" ", errorMessages));
        }

        // Save the employee
        // ...

        return ResponseEntity.ok("Employee saved successfully");
    }
}

Наш контроллер REST (EmployeeController ) обрабатывает запрос POST для сохранения сотрудника. Метод save() принимает объект EmployeeRequest в теле запроса, выполняет проверку с использованием аннотации @Valid и собирает все ошибки проверки с помощью BindingResult. Затем метод создает строку с сообщениями об ошибках. и возвращает его в качестве ответа.

5. Модульные тесты

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

@SpringBootTest
class MultipleOfFiveValidatorTests {


        @Test
        public void testValidValue() {
            MultipleOfFiveValidator validator = new MultipleOfFiveValidator();
            assertTrue(validator.isValid(10, null));
            assertTrue(validator.isValid(0, null));
            assertTrue(validator.isValid(-15, null));
        }

        @Test
        public void testInvalidValue() {
            MultipleOfFiveValidator validator = new MultipleOfFiveValidator();
            assertFalse(validator.isValid(7, null));
            assertFalse(validator.isValid(18, null));
            assertFalse(validator.isValid(-9, null));
        }

}