Jednym z podstawowych zadań każdej aplikacji jest sprawdzanie danych wejściowych. Dlatego każda aplikacja powinna być wyposażona w uniwersalny mechanizm, który taką walidację zapewni. Spring Framework ma wbudowane dwa takie mechanizmy, o tym właśnie możesz przeczytać w tym artykule.
Pierwszy z nich korzysta ze specyfikacji Bean Validation (JSR 380), drugi z mechanizmów Springa DataBinder. Oczywiście, możemy jeszcze zaimplementować swoje własne mechanizmy np. oparte o AOP (Aspect Oriented Programming), bądź najprościej jak się da, walidować obiekty ręcznie, sprawdzając ich pola i obsługiwać błędy.
Natomiast te dwa mechanizmy są wbudowane w framework i warto mieć je dobrze opanowane – na pewno się przydadzą.
Walidacja za pomocą Bean Validation
Historia Bean Validation sięga roku 2009, gdzie została wydana pierwsza wersja tej specyfikacji (JSR 303). W 2017 mieliśmy kolejną wersję specyfikacji (Bean Validation 2.0, JSR 380), która rozszerzała istniejący standard o nowe elementy, między innymi wsparcie dla Javy 8).
Idea działania tego mechanizmu jest bardzo prosta. Umieszczasz odpowiednią adnotację np. @NotNull na polu klasy, wrzucasz taki obiekt do walidatora, który implementuje ten standard i gotowe.
A jak wygląda to w Springu ?
class Person {
@NotEmpty
private String name;
@Min(18)
private int age;
@Email
private String email;
// gettery i settery
}I w kontrolerze:
@PostMapping("/person")
public ResponseEntity<Object> savePerson(@RequestBody @Valid Person personDto) {
// ... implementacja ...
return ResponseEntity.ok().build()
}
Uwaga: Konieczna tutaj jest adnotacja
@Valid. Pisałem już o tym w: Najczęściej popełniane błędy w Spring Framework.
O ile ten mechanizm i podstawowe walidatory są znane większości developerów, tak przynajmniej zakładam, to ostatnia specyfikacja wprowadza kilka ciekawych nowości:
- walidacje obiektów zawartych w kolekcjach, poprzez adnotowanie typów generycznych
List<String> emailList, - wsparcie walidacji w Optional
@PastOptional<LocalDate>, - wsparcie dla Java 8 Date/Time API poprzez nowe adnotacje
@Past,@Future,@PastOrPresenti@FutureOrPresent, - nowe adnotacje
@Email,@NotEmpty,@NotBlank,@Positive,@PositiveOrZero,@Negative,@NegativeOrZero.
Customowe adnotacje / validatory
Mechanizm ten pozwala na tworzenie własnych adnotacji (walidatorów). Dzięki czemu możemy walidować bardziej zindywidualizowane dane, lub robić walidację krzyżową na różnych polach.
Przykładowa implementacja takiej adnotacji:
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy =MyCustomeValidator.class)
public @interface MyCustomeAnnotation {
String message() default "{mycustom.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}oraz implementacja walidatora:
public class IbanValidator implements ConstraintValidator<MyCustomAnnotation, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// ... implementacja ...
}
}Taką walidację można też wywołać ręcznie, w dowolnym miejscu aplikacji, używając ValidatorFactory:
Validation.buildDefaultValidatorFactory().getValidator().validate(obj)
Domyślną implementacja Bean Validation w Springu jest Hibernate Validator. Można go też używać niezależnie od Springa.
Walidacja za pomocą Spring validation
Kolejnym sposobem walidacje w Springu jest DataBinding. Jest to mechanizm Spring Validation pozwalający przyporządkować walidator do obiektu poprzez WebDataBinder.
Przykładowa implementacja walidatora w Springu, implementuje interfejs org.springframework.validation.Validator:
public class MyCustomValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return PersonDto.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
PersonDto person = (PersonDto) target;
if (person.getName().equals("admin")) {
errors.reject("name", "Nie możesz użyć nazwy admin");
}
}
}Walidatory tego typu można używać do bardziej skomplikowanych przypadków, w których adnotacje Bean Validation nie wystarczają.
@RestController
public class MyController {
@InitBinder("personDto")
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new MyCustomValidator());
}
@PostMapping("/binder")
public ResponseEntity<Object> addPerson(@RequestBody @Valid PersonDto person) {
// ... implementacja ...
return ResponseEntity.ok().build()
}
}WebDataBinder i adnotacja @InitBinder("personDto") podłącza walidator do obiektu requestu PersonDto. Powiązanie to jest tworzone poprzez nazwę typu (PersonDto -> personDto – konwencja Springowa). Gdy nie podamy do jakiego parametru chcemy powiązać walidator, wtedy zostanie on podłączony do wszystkich obiektów wejściowych.
Dodatkowo, możemy tutaj skorzystać z klasy ValidationUtils, która zawiera metody pomagające sprawdzać pola walidowanego obiektu.
Uwaga: W tym wypadku również konieczna jest adnotacja
@Valid. Kolejną rzeczą, na którą trzeba zwrócić uwagę, to użycie metodyaddValidators(...). Jeśli zamiast niej użyjeciesetValidator(...), to wszystkie pozostałe walidacje (np. Bean Validation), zostaną wyłączone. Kiedyś niestety popełniłem ten błąd i kosztowało mnie to kilka godzin debugowania.
Podsumowanie
Walidacja to podstawa każdej, solidnie napisanej aplikacji. Dlatego warto zagłębić się w ten temat. A już niedługo na blogu pojawią się kolejne artykuły poświęcone nie tylko temu zagadnieniu, ale także Spring Framework, Hibernate i testom jednostkowym.
Źródła:
https://beanvalidation.org/2.0/
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-config-validation







Siema, mam pytanie. Chcę podać 2 argumenty z RequestBody, ale to niestety nie zadziała. Szukałam rozwiązania w necie jak złączyć w jedno, ale wszędzie są stare komentarze bez szczegółów przez co nie wiem jak do tego podejść. Z góry dzięki za odp.
Tak się raczej nie robi. Zrób klasę DTO, która będzie zawierała argument 1 i agrument 2. Później możesz sobie wyciągnąć z tej klasy oba argumenty.