Перевірка даних — 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 все аналогічно
Далі перевіряти так:
@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>
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