Проверка данных класса (bean) в java тема не новая, но актуальная и здесь я объединю различные аспекты: валидацию данных в рамках JSR-303, покажу как это сделать чисто в Java и с использованием Spring, как делать в стандартном приложении и в Web.
Содержание: Валидация данных (JSR-303) в
Для проверки объекта используются аннотации на полях класса, т.е. декларативная модель. Аннотации есть уже готовые:
Здесь в примере Size и @Digits готовые аннотации, а @PersonAgeConstraint собственная. Как сделать собственную:
— подготавливаем аннотацию
В message() указываем ключ (value.negative) из файла ресурса (ValidationMessages.properties) для сообщения
Моя собственная аннотация готова, добавляем ее к полю и уже можно проверить, все поля на которых есть аннотации будут проверены соответствующими правилами.
Результат в консоли
Сообщения для стандартных аннотаций можно указать в файле сообщений, по правилу:
Структура проекта

Поэтапная проверка. Для Class<?>[] groups() можно указывать типы классов по которым можно потом группировать, ограничивать список проверок, т.е. использовать как фильтр. Таким образом проверку можно сделать по этапам, 1) Например разделим проверку лица по состоян��ю здоровья, 2) а уже затем профессиональные данные. Подготовим две аннотации
HealthConstraint и ProfessionalConstraint и реализации для них. Первым проверим соответствие здоровью а затем если проходит по здоровью, проверим на профессиональные данные.
Пример:
Пример аннотации HealthConstraint
Пример реализации HealthConstraintValidator
для ProfessionalConstraint все аналогично
Далее проверять так:
Подобные проверки, например нужны когда мы загружаем данные из файла, web service и др. источников.
В Spring есть так же свой интерфейс Validator
Переопределив два метода, делаем валидацию
value.negative — так же является ключом в файле сообщений,
Проверка запускается через DataBinder
Пример:
Будут выполнены все проверки которые имплементировали org.springframework.validation.Validator для класса Person.
Можно добавить так же несколько валидаторов, dataBinder.addValidators, можно сделать композицию правил (вызов из одного правила, другого), пример:
Я почему то ожидал, Spring будет выполнять также проверки указанные в аннотациях, но нет, этот вызов надо делать самостоятельно.
Структура проекта

Очевидно я захочу использовать два подхода в проверки данных — Java и Spring, объединить их можно, а именно добавить в Spring validator вызов javax.validation.Validator.
Пример
С помощью spring делаем injection
далее на методе
выполняем декларативные проверки java, а затем выполняем все проверки для класса Person на spring org.springframework.validation.Validator.
Запускаем проверку также через spring
Теперь в коллекции будут проверки от аннотаций java и spring (org.springframework.validation.Validator) для Person
Вывод в консоли
Структура проекта

Конечно теперь это все можно применить в web приложении.
Добавляем в проект Controller, jsp страницу (тут кстати могут и другие варианты, например генерация страниц с помощью freeMarker, и др.), css стиль, pom зависимость. И так по порядку
1) MVC Controller
Здесь с помощью spring injection подключен PersonValidator
устанавливаем PersonValidator в initBinder
Проверка инициируется с помощью аннотации
В этом случае выполнится только spring проверка, декларативные проверки будут проигнорированы.
Если убрать из кода
то наоборот выполнятся все декларативные проверки, а spring будут проигнорированы.
Что бы выполнить все проверки и декларативные и spring, можно поступить так:
Убрать @InitBinder, оставить injection
и добавить вызов spring проверки вручную
Вот код:
т.е. в bindingResult будут добавлены еще проверки от spring :-), что и хотелось!
Привязка данных в jsp и модели, осуществляется атрибутом -
Остальные ресурсы этого примера:
Spring configuration
Структура проекта

Работа приложения

Материалы
Bean Validation specification
Содержание: Валидация данных (JSR-303) в
- стандартном Java приложении
- c использованием 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=Отрицательное\u0020значениеи реализацию класса проверки — 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.reporting.outputEncoding>UTF-8</project.reporting.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.reporting.outputEncoding>UTF-8</project.reporting.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.reporting.outputEncoding>UTF-8</project.reporting.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
