Перевірка даних — Java & Spring Validation

Перевірка даних класу (bean) в java тема не нова, але актуальна і тут я об’єднаю різні аспекти: валідацію даних в рамках JSR-303, покажу як це зробити чисто в Java і з використанням Spring, як робити в стандартному додатку і в Web.
Зміст: Валідація даних (JSR-303)
— стандартному додатку Java
— з використанням Spring
— об’єднання Java + Spring
— Spring MVC

Validation в стандартному додатку Java

Для перевірки об’єкта анотації використовуються на полях класу, тобто декларативна модель. Анотації є вже готові: Null @DecimalMin, @Digits, Pattern, Email і ін, а також можна робити і власні. І так є клас (bean)

import javax.validation.constraints.Digits;
import javax.validation.constraints.Size;

public class Person {

 @Size(min=2, max=50)
 private String Name;

 @Digits(integer=3, fraction=0, message = "Не більше 3-х знаків")
@PersonAgeConstraint
 private Integer age;

 public Person(String name, Integer age) {
 Name = name;
 this.age = age;
}
}

Тут у прикладі Size і @Digits готові анотації, а @PersonAgeConstraint власна. Як зробити власну:
— готуємо анотацію

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=PersonAgeConstraintValidator.class)
public @interface PersonAgeConstraint {
 String message() default "{value.negative}";
 Class<?>[] groups() default {};
 Class<? extends Payload>[] payload() default {};
}

У message() вказуємо ключ (value.negative) з файлу ресурсу (ValidationMessages.properties) для повідомлення
value.negative=Негативнеи0020значение
і реалізацію класу перевірки — PersonAgeConstraintValidator.class

public class PersonAgeConstraintValidator implements ConstraintValidator<PersonAgeConstraint, Integer> {
@Override
 public boolean isValid(Integer age, ConstraintValidatorContext constraintValidatorContext) {
 return age > 0;
}
}

Моя власна анотація готова, додаємо її до поля і вже можна перевірити, усі поля, на яких є анотації будуть перевірені відповідними правилами.

import javax.validation.Validator;
/**
 * Test Validation
*/
public class DemoJValidationApplicationTests {

 // Ініціалізація Validator
 private static Validator validator;
 static {
 ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
 validator = validatorFactory.usingContext().getValidator();
}

@Test
 public void testValidators() {
 final Person person = new Person("Іван Петров", -4500);

 Set<ConstraintViolation<Person>> validates = validator.validate(person);
 Assert.assertTrue(validates.size() > 0);
 validates.stream().map(v -> v.getMessage())
.forEach(System.out::println);
}
}

Результат в консолі
Не більше 3-х знаків
Від'ємне значення

Повідомлення для стандартних анотацій можна вказати у файлі повідомлень, за правилом:
AnnotationName.entity.fieldname=повідомлення
Структура проекту

pom файл

<?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>com.example</groupId>
<artifactId>DemoJSRvalidation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>DemoJSRvalidation</name>
 <description>Demo project for Spring Boot JSR-303 validation</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.звітування.outputEncoding>UTF-8</project.звітування.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>


</project>

Поетапна перевірка. Для Class<?>[] groups() можна вказувати типи класів з яким потім можна групувати, обмежувати список перевірок, тобто використовувати як фільтр. Таким чином перевірку можна зробити за етапами, 1) Наприклад розділимо перевірку особи за станом здоров’я, 2) а вже потім професійні дані. Підготуємо дві анотації
HealthConstraint і ProfessionalConstraint та реалізації для них. Першим перевіримо відповідність здоров’ю а потім якщо проходить по здоров’ю, перевіримо на професійні дані.
приклад:

public class Person {

 @HealthConstraint(groups = Health.class)
 private Documents healthDocuments;

 @ProfessionalConstraint(groups = Professional.class)
 private Documents ProfessionalDocuments;
//...
}

приклад анотації HealthConstraint

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=HealthConstraintValidator.class)
public @interface HealthConstraint {
 String message() default "{health.documents}";
 Class<?>[] groups() default {};
 Class<? extends Payload>[] payload() default {};
}

приклад реалізації HealthConstraintValidator

public class HealthConstraintValidator implements ConstraintValidator<HealthConstraint, Documents> {
@Override
 public boolean isValid(Documents documents, ConstraintValidatorContext constraintValidatorContext) {
 return documents.contains("довідка 1");
}
}

для ProfessionalConstraint все аналогічно

Читайте також  Огляд уразливості в Winbox від Mikrotik. Або великий фейл

Далі перевіряти так:

@Test
 public void healthAndProfessionalValidators() {
 final Person person = new Person("Іван Петров", 45);
 person.setHealthDocuments(new Documents(Arrays.asList("довідка 1", "довідка 3")));
 person.setProfessionalDocuments(new Documents(Arrays.asList("тест 1", "тест 4")));

 // перевірка на здоров'я
 Set<ConstraintViolation<Person>> validates = validator.validate(person, Health.class);
 Assert.assertTrue(validates.size() == 0);

 // та якщо здоров'я Ок, то проф. тест
 validates = validator.validate(person, Professional.class);
 Assert.assertTrue(validates.size() == 0); 

}

Подібні перевірки, наприклад потрібні коли ми завантажуємо дані з файлу, web service та ін. джерел.
клас Documents

public class Documents {

 private List<String> tests = new ArrayList();

 public Documents(List<String> tests) {
this.tests.addAll(tests);
}

 public boolean contains(String test) {
 return this.tests.contains(test);
}
}

Validation c використанням Spring

В Spring є так само свій інтерфейс Validator (org.springframework.validation.Validator) як і в Java (javax.validation.Validator) і саме його імплементація виконує перевірку даних. Це вже не декларативний підхід, але в ньому є своя гнучкість і розширюваність. Для того ж біна, зроблю тугіше перевірку віку.
Якщо перевизначити два методу, робимо валідацію

@Service
public class PersonValidator implements Validator {
@Override
 public boolean supports(Class<?> aClass) {
 return Person.class.equals(aClass);
}

@Override
 public void validate(Object obj, Errors errors) {
 Person p = (Person) obj;
 if (p.getAge() < 0) {
 errors.rejectValue("age", "value.negative");
}
}
}

value.negative — так само є ключем у файлі повідомлень, public boolean supports визначає тип підтримуваної класу.
Перевірка запускається через DataBinder
Приклад:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoJValidationApplicationTests {
// вказуємо файл повідомлень 
private static final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
 static {
messageSource.setBasename("message");
}

@Autowired
 private PersonValidator personValidator;

@Test
 public void testValidators() {
 final Person person = new Person("Іван Петров", -4500);

 final DataBinder dataBinder = new DataBinder(person);
dataBinder.addValidators(personValidator);
dataBinder.validate();

Assert.assertTrue(dataBinder.getBindingResult().hasErrors());

 if (dataBinder.getBindingResult().hasErrors()) {
dataBinder.getBindingResult().getAllErrors().stream().
 forEach(e -> System.out.println(messageSource
 .getMessage(e, Locale.getDefault())));
}
}
}

Будуть виконані всі перевірки, які імплементували org.springframework.validation.Validator для класу Person
Можна додати так само кілька валідаторів, dataBinder.addValidators, можна зробити композицію правил (виклик з одного правила, іншого), приклад:

public class OtherValidator implements Validator {

@Override
 public boolean supports(Class<?> aClass) {
 return Person.class.equals(aClass);
}

@Override
 public void validate(Object obj, Errors errors) {
 // ...
}
}
//---------
@Service
public class PersonValidator implements Validator {

/**
 * інше правила
 */ 
@Autowired
 private OtherValidator otherValidator;

@Override
 public void validate(Object obj, Errors errors) {
 Person p = (Person) obj;
 if (p.getAge() < 0) {
 errors.rejectValue("age", "value.negative");
}
 // з одного правила, викликаємо інше
 otherValidator.validate(obj, errors);
}
}

Я чомусь очікував, Spring буде виконувати також перевірки вказані в анотаціях, але ні, цей виклик треба робити самостійно.
Структура проекту

pom файл

<?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>com.example</groupId>
<artifactId>DemoJSRvalidation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>DemoJSRvalidation</name>
 <description>Demo project for Spring Boot JSR-303 validation</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.звітування.outputEncoding>UTF-8</project.звітування.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>


</project>

Читайте також  Підсилювачі низької частоти класів: А, B, AB, D, G, H

Java & Spring

Очевидно я захочу використовувати два підходу до перевірки даних Java Spring, об’єднати їх можна, а саме додати в Spring validator виклик javax.validation.Validator
Приклад

import javax.validation.Validator;

@Service
public class PersonValidator implements org.springframework.validation.Validator {

 // javax.validation.Validator
@Autowired
 private Validator validator;

@Override
 public boolean supports(Class<?> aClass) {
 return Person.class.equals(aClass);
}

@Override
 public void validate(Object obj, Errors errors) {
 Set<ConstraintViolation<Object>> validates = validator.validate(obj);

 for (ConstraintViolation<Object> constraintViolation : validates) {
 String propertyPath = constraintViolation.getPropertyPath().toString();
 String message = constraintViolation.getMessage();
 errors.rejectValue(propertyPath, "", message);
}

 Person p = (Person) obj;
 if (p.getAge() < 0) {
 errors.rejectValue("age", "only.positive.numbers");
}
}
}

З допомогою spring робимо injection javax.validation.Validator
@Autowired
private Validator validator;
далі на метод public void validate(Object obj, Errors errors)
виконуємо декларативні перевірки java, а потім виконуємо всі перевірки для класу Person на spring org.springframework.validation.Validator.
Запускаємо перевірку також через spring

@Test
 public void testValidators() {
 final Person person = new Person("Іван", -4500);

 final DataBinder dataBinder = new DataBinder(person);
dataBinder.addValidators(personValidator);
dataBinder.validate();

 if (dataBinder.getBindingResult().hasErrors()) {
dataBinder.getBindingResult().getAllErrors()
 // ....

тепер у колекції будуть перевірки від анотацій java spring (org.springframework.validation.Validator) для Person
Висновок в консолі
Від'ємне значення (анотація)
Не більше 3-х знаків (анотація)
Тільки позитивні число (spring)

Структура проекту

Spring MVC

Звичайно тепер це все можна застосувати web додатку.
Додаємо в проект Controller, jsp сторінку (тут до речі можуть і інші варіанти, наприклад генерація сторінок за допомогою freeMarker, і ін), css стиль, pom залежність. І так по порядку
1) MVC Controller

import org.springframework.validation.Validator;

@Controller
public class DemoJValidationController {

@Autowired
 @Qualifier("personValidator") // spring validator
 private Validator personValidator;

@InitBinder
 protected void initBinder(WebDataBinder binder) {
binder.setValidator(personValidator);
}

@GetMapping("/")
 public String savePersonAction(ModelMap model) {
 model.addAttribute("person", new Person(null, null));
 return "personEdit";
}

 @RequestMapping(value = "/save", method = RequestMethod.POST)
 public String savePersonAction(
 @Valid @ModelAttribute("person") Person person,
 BindingResult bindingResult, Model model) {
 if (bindingResult.hasErrors()) {
 return "personEdit"; // to person.jsp page
}

 model.addAttribute("name", person.getName());
 model.addAttribute("age", person.getAge());
 return "saveSuccess"; // to saveSuccess.jsp page
}

 @RequestMapping(value = "/edit", method = RequestMethod.POST)
 public String editPersonAction(ModelMap model) {
 model.addAttribute("person", new Person(null, null));
 return "personEdit"; // to personEdit.jsp page;
}

}

Тут з допомогою spring injection підключений PersonValidator
@Autowired
@Qualifier("personValidator") // spring validator
private Validator personValidator;

встановлюємо PersonValidator в initBinder
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(personValidator);
}

Перевірка ініціюється з допомогою анотації @Valid
У цьому випадку виконається тільки spring перевірка, декларативні перевірки будуть проігноровані.
Якщо прибрати з коду
@InitBinder
protected void initBinder(WebDataBinder binder)

навпаки виконаються всі декларативні перевірки, а spring будуть проігноровані.
Що б виконати всі перевірки і декларативні spring, можна вчинити так:
Прибрати @InitBinder, залишити injection
@Autowired
@Qualifier("personValidator") // spring validator
private Validator personValidator;

і додати виклик spring перевірки вручну
// spring validate
personValidator.validate(person, bindingResult);
Ось код:

@Controller
public class DemoJValidationController {

@Autowired
 @Qualifier("personValidator") // spring validator
 private Validator personValidator;
//...
 @RequestMapping(value = "/save", method = RequestMethod.POST)
 public String savePersonAction(
 @Valid @ModelAttribute("person") Person person,
 BindingResult bindingResult, Model model) {
 // spring validate
 personValidator.validate(person, bindingResult);

 if (bindingResult.hasErrors()) {
 return "personEdit"; // to person.jsp page
}

 model.addAttribute("name", person.getName());
 model.addAttribute("age", person.getAge());
 return "saveSuccess"; // to saveSuccess.jsp page
}
}

тобто в bindingResult будуть додані ще перевірки від spring :-), що й хотілося!
Прив’язка даних в jsp і моделі, здійснюється атрибутом – modelAttribute="person" У прикладі підключена SpringMVC’s Form Tag Library.
Інші ресурси цього прикладу:
DemoJValidationApplication

@SpringBootApplication
@ImportResource("classpath:configuration.xml")
public class DemoJValidationApplication {

 public static void main(String[] args) {
 SpringApplication.run(DemoJValidationApplication.class, args);
}
}

Spring configuration
configuration.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/c"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">


 <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
 <property name="basename" value="classpath:message"/>
 <property name="defaultEncoding" value="UTF-8"/>
</bean>

<mvc:annotation-driven/>
 <mvc:resources mapping="/resources/**" location="classpath:/META-INF/resources/"/>
</beans>

personEdit.jsp

<%@ page language="java"
 contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
 <link href="<c:url value="/resources/my.css" />" rel="stylesheet">
<title>Person</title>
</head>
<body>
<h3>
 Enter Person.
</h3>

 <form:form method="POST" modelAttribute="person" action="save">
<div>
Name:
 <form:input path="name"/>
 <form:errors path="name" cssClass="error"/>
</div>

<div>
Age:
 <form:input path="age"/>
 <form:errors path="age" cssClass="error"/>
</div>

 <button type="submit">Registration</button>
</form:form>

</body>
</html>

saveSuccess.jsp

<%@ page language="java"
 contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
 <link href="<c:url value="/resources/my.css" />" rel="stylesheet">
 <title>Person Saved Successfully</title>
</head>
<body>
<h3>
 Person Saved Successfully.
</h3>

<form:form method="POST" modelAttribute="person" action=edit>
<div>
${name}
</div>
<div>
${age}
</div>
 <button type="submit">Edit</button>
</form:form>
</body>
</html>

my.css

span.error {
 color: red;
}
form div{
 margin: 5px;
}

pom файл

<?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>com.example</groupId>
<artifactId>DemoJSRvalidation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>DemoJSRvalidation</name>
 <description>Demo project for Spring Boot JSR-303 validation</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.звітування.outputEncoding>UTF-8</project.звітування.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.1.0.Final</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

</plugins>
</build>
</project>

Структура проекту

Робота програми

Матеріали
Bean Validation specification

Читайте також  Як створити надійну ігрову механіку, користуючись тільки Excel: моделювання та оптимізація рішень

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *