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
,@PastOrPresent
i@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.