Если вы используете Spring Validation, то вы скоро поймёте, что аннотаций @Size, @NotNull и прочих вам не хватает. Вы, разумеется, можете написать свой наследник класса org.springframework.validation.Validator, но так не хочется терять красоту и лаконичность аннотаций.
К счастью, Spring Framework позволяет расширять имеющиеся аннотации для проверки значений своими собственными, самописными. В этой статье я приведу пример создания подобной аннотации. Более того, наша аннотация будет обращаться к бину из слоя сервисов при проверке значения. Подобную технику можно использовать, например, для проверки уникальности введённых пользователем значений по какому-нибудь полю.
Предположим, что нам нужно проверить следующий класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package ru.urvanov.javaexamples.customconstraintvalidator.domain; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class Person { private Long id; @NotNull @Size(min = 1, max = 100) private String name; @Size(min = 1, max = 13) private String itn; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getItn() { return itn; } public void setItn(String itn) { this.itn = itn; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + ", itn='" + itn + '\'' + '}'; } } |
Большую часть проверок мы уже сделали аннотациями, но осталось ещё кое-что. Вполне ожидаемо, что заказчик захочет сделать проверку уникальности ИНН. В данном случае существующими стандартными аннотациями из javax.validation не обойдёшься, более того, нам нужно обращаться к базе данных для проверки существования записи с введённым ИНН. В таком случае нам нужно написать свою аннотацию. Делается это так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package ru.urvanov.javaexamples.customconstraintvalidator.validator.person; import java.lang.annotation.*; import javax.validation.*; @Documented @Constraint(validatedBy = UniqueItnConstraintValidator.class) @Target( { ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface UniqueItn { String message() default "ru.urvanov.javaexamples.customconstraintvalidator.validation.unique.itn"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
Обратить внимание нужно на следующее:
- @Constraint(validatedBy = здесь указывается сам класс, который будет осуществлять проверку в соответствии с нашей новой аннотацией.
- String message() — здесь мы указываем сообщение об ошибке по умолчанию.
- groups() и payload() в данном случае не пригодятся, поэтому они пустые.
В классе UniqueItnConstraintValidator осуществляется сама проверка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package ru.urvanov.javaexamples.customconstraintvalidator.validator.person; import org.springframework.beans.factory.annotation.Autowired; import ru.urvanov.javaexamples.customconstraintvalidator.domain.Person; import ru.urvanov.javaexamples.customconstraintvalidator.service.PersonService; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class UniqueItnConstraintValidator implements ConstraintValidator<UniqueItn, Person> { @Autowired private PersonService personService; @Override public void initialize(UniqueItn uniqueItn) { } @Override public boolean isValid(Person person, ConstraintValidatorContext ctx) { if (personService.findByItn(person.getItn()).map(p -> p.getId()) .filter(id -> !id.equals(person.getId())).isPresent()) { ctx.disableDefaultConstraintViolation(); ctx.buildConstraintViolationWithTemplate( "{ru.urvanov.javaexamples.customconstraintvalidator.validation.unique.itn}") .addPropertyNode("itn").addConstraintViolation(); return false; } return true; } } |
На что обратить внимание:
- Наследуемся от javax.validation.ConstraintValidator
- Внедряем свой бин PersonService, к которому обращаемся для проверки существования записи с таким ИНН.
- Отключаем обработку по умолчанию с помощью disableDefaultConstraintViolation().
- Формируем свою информацию об ошибке валидации с указанием поля “itn” и кода сообщения об ошибке в фигурных скобках "{ru.urvanov.javaexamples.customconstraintvalidator.validation.unique.itn}".
- Возвращаем false.
- Если бы мы просто вернули false без отключения стандартного поведения и создания своей ошибки с указанием поля, то ошибка относилась бы ко всему типу Person, а не к конкретному его полю.
Само сообщение для кода ru.urvanov.javaexamples.customconstraintvalidator.validation.unique.itn хранится в файле “src/main/resources/ValidationMessages.properties” (а локализованные версии в файлах “src/main/resources/ValidationMessages_ru_RU.properties” и аналогичных):
1 |
ru.urvanov.javaexamples.customconstraintvalidator.validation.unique.itn=Duplicate itn. |
Созданную аннотацию указываем для всего класса Person:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package ru.urvanov.javaexamples.customconstraintvalidator.domain; import ru.urvanov.javaexamples.customconstraintvalidator.validator.person.UniqueItn; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @UniqueItn public class Person { private Long id; ... } |
Наш контекст Spring-а:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/> <bean id="personService" class="ru.urvanov.javaexamples.customconstraintvalidator.service.PersonServiceImpl" /> </beans> |
Главный класс приложения, в котором мы инициализируем контекст Spring-а и запускаем проверку для экземпляра Person:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package ru.urvanov.javaexamples.customconstraintvalidator; import org.springframework.context.support.GenericXmlApplicationContext; import ru.urvanov.javaexamples.customconstraintvalidator.domain.Person; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.util.Set; public class App { public static void main(String[] args) { try (GenericXmlApplicationContext context = new GenericXmlApplicationContext()) { context.load("classpath:applicationContext.xml"); context.refresh(); Validator validator = (Validator) context.getBean("validator"); Person person = new Person(); person.setName("Vasya"); person.setItn("duplicate itn"); Set<ConstraintViolation<Person>> constraintViolationSet = validator.validate(person); for (ConstraintViolation<Person> constraintViolation : constraintViolationSet) { System.out.println("constraintViolation.message = " + constraintViolation.getMessage()); System.out.println("constraintViolation.field = " + constraintViolation.getPropertyPath()); } } } } |
В нашем примере мы получаем множество Set<ConstraintViolation<Person>>, но при ошибках маппинга в контроллерах Spring MVC мы будем получать результат BindingResult!
Файл “pom.xml” с зависимостями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ru.urvanov.javaexacontexts</groupId> <artifactId>custom-constraint-validator</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java-version>1.8</java-version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <org.springframework-version>5.0.1.RELEASE</org.springframework-version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework-version}</version> </dependency> <!-- Реализация Validation API --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.4.Final</version> </dependency> <!-- Если вы деплоите приложение в контейнере Tomcat или GlassFish, то эта зависимость будет поставляться контейнером. В нашем же случае приходится подключать напрямую.--> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>javax.el</artifactId> <version>2.2.6</version> </dependency> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.1-b04</version> </dependency> </dependencies> <build> <plugins> <!-- Компиляция --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>${java-version}</source> <target>${java-version}</target> <encoding>${project.build.sourceEncoding}</encoding> <compilerArgument>-Xlint:all</compilerArgument> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin> <!-- Сборка JAR-файла --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>ru.urvanov.javaexamples.customconstraintvalidator.App</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>data/lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin> <plugin> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <phase>install</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/data/lib</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> |
При запуске метода main из “App.java” в консоль будет выведено следующее:
1 2 |
constraintViolation.message = Duplicate itn. constraintViolation.field = itn |
Ссылки:
ZIP-архив с исходными кодами проекта
Буду рад любым комментариям!
Не могли бы пояснить метод isValid(….)
Что там происходит, изначально у нашего Persona нет id и он единственный , то есть везде предается null? в итоге метод isPresent() всегда вернет false. Обычно в базе у всех Person есть id (PK). Смысл, я так понимаю проверить реальное совпадение и дублирование именно itn?
Да, верно. Мы берём id персоны, найденной в базе. В базе id всегда есть. Сравниваем с id нашей Person, у нашей id вполне может быть равен null, то тогда равенство вернёт false. Если id персоны с этим ИНН в базе и нашей персоны не равны, то генерируем ошибку.
Смысл именно в проверке дублирования ИНН.
Добрый день! Могли бы Вы подсказать, есть ли способ передавать из Constraint разные сообщения(вместо default). К примеру, мы валидируем компонент и нам нужно отправлять разные сообщения для каждого поля.
Думаю, что в вашем случае можно создать свою аннотацию валидации не для всего класса, а для поля. А сообщение передавать не в default, а писать для каждого поля отдельно.
Либо надо смотреть в возможность передачи в ctx.buildConstraintViolationWithTemplate разных сообщений для каждого поля.