Проверка данных — Java & Spring Validation
Проверка данных класса (bean) в java тема не новая, но актуальная и здесь я объединю различные аспекты: валидацию данных в рамках JSR-303, покажу как это сделать чисто в Java и с использованием Spring, как делать в стандартном приложении и в Web.
Содержание: Валидация данных (JSR-303) в
- стандартном Java приложении
- c использованием Spring
- объединение Java + Spring
- Spring MVC
Validation в стандартном Java приложении
Для проверки объекта используются аннотации на полях класса, т.е. декларативная модель. Аннотации есть уже готовые:
а также можно делать и собственные. И так есть класс (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() @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=PersonAgeConstraintValidator.class) public @interface PersonAgeConstraint < String message() default ""; Class[] groups() default <>; Class extends Payload>[] payload() default <>; >
Пример реализации HealthConstraintValidator
public class HealthConstraintValidator implements ConstraintValidator < @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> validates = validator.validate(person, Health.class); Assert.assertTrue(validates.size() == 0); // и если здоровье Ок, то проф. тест validates = validator.validate(person, Professional.class); Assert.assertTrue(validates.size() == 0); >
Подобные проверки, например нужны когда мы загружаем данные из файла, web service и др. источников.
public class Documents < private Listtests = new ArrayList(); public Documents(List tests) < this.tests.addAll(tests); >public boolean contains(String test) < return this.tests.contains(test); >>
Validation c использованием Spring
и именно его имплементация выполняет проверку данных. Это уже не декларативный подход, но в нем есть своя гибкость и расширяемость. Для того же бина, сделаю туже проверку возраста.
Переопределив два метода, делаем валидацию
@Service public class PersonValidator implements Validator < @Override public boolean supports(ClassaClass) < 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(ClassaClass) < 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 будет выполнять также проверки указанные в аннотациях, но нет, этот вызов надо делать самостоятельно.
4.0.0 com.example DemoJSRvalidation 0.0.1-SNAPSHOT jar DemoJSRvalidation Demo project for Spring Boot JSR-303 validation org.springframework.boot spring-boot-starter-parent 2.0.5.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin
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(ClassaClass) < return Person.class.equals(aClass); >@Override public void validate(Object obj, Errors errors) < Set> validates = validator.validate(obj); for (ConstraintViolation 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 зависимость. И так по порядку
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.
Остальные ресурсы этого примера:
@SpringBootApplication @ImportResource("classpath:configuration.xml") public class DemoJValidationApplication < public static void main(String[] args) < SpringApplication.run(DemoJValidationApplication.class, args); >>
" rel="stylesheet"> Enter Person.
Name: Age:
" rel="stylesheet"> Person Saved Successfully.
$ $
4.0.0 com.example DemoJSRvalidation 0.0.1-SNAPSHOT jar DemoJSRvalidation Demo project for Spring Boot JSR-303 validation org.springframework.boot spring-boot-starter-parent 2.0.5.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.hibernate hibernate-validator 4.1.0.Final org.apache.tomcat.embed tomcat-embed-jasper javax.servlet jstl org.springframework.boot spring-boot-maven-plugin