Wybierz region
pl
  • PL
  • EN
Wydrukuj

Wszystko, co w skrócie warto wiedzieć o Bean Validation

W dzisiejszym wpisie zajmiemy się zagadnieniem walidacji, czyli zapewnienia poprawności przetwarzanych danych. W przypadku programów pisanych w Javie – szeroko wykorzystywanej do tworzenia aplikacji webowych – z problemem walidacji przychodzi mierzyć się programistom praktycznie każdego dnia.

Konieczne jest m.in. zapewnienie prawidłowości danych, które trafiają do aplikacji z front endu, a także zagwarantowanie, że tablice bazodanowe wypełnianie będą zgodnie z zadeklarowanymi typami. Każdorazowe sprawdzanie poprawności za pomocą instrukcji warunkowych zwartych w kodzie biznesowym byłoby uciążliwe, dlatego w Javie wypracowano specyfikację o nazwie Bean Validation, która pozwala walidować parametry, wartości zwracane przez metody czy konstruktory z wykorzystaniem wygodnych adnotacji.

Specyfikacja Bean Validation dostarcza grupę gotowych walidatorów, takich jak np. @NotNull (zapewniający, że wartość nim opatrzona nie będzie nullem) czy @PastOrPresent, którego zadaniem jest zagwarantowanie, że data nim opatrzona jest datą obecną lub przeszłą. Specyfikacja ta pozwala jednak na znacznie więcej – w szczególności na modyfikowanie istniejących oraz tworzenie własnych walidatorów.

Aby zobrazować, w jaki sposób możliwe jest tworzenie własnych walidatorów, wyobraźmy sobie obiekt reprezentujący adres klienta. W ramach walidacji chcemy sprawdzić, czy uzupełnione jest minimum adresowe. Ponieważ zakładamy, ze minimum adresowe dla adresów polskich jest inne niż dla adresów zagranicznych, nie możemy operować na dostępnych walidatorach, lecz stworzyć musimy własny, w ramach którego sprawdzimy kompleksowo prawidłowość całego obiektu reprezentującego adres (sprawdzimy, czy pole z nazwą państwa ma wartość „Polska” czy inną wartość i zależnie od tego zweryfikujemy wypełnienie pozostałych pól adresu). Aby utworzyć taki walidator w pierwszej kolejności musimy zdefiniować samą adnotację, np. w taki sposób:

@Constraint(validatedBy = AddressMinimumValidator.class)

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

public @interface AddressMinimum {

 

String message() default "The address minimum is not filled";

 

Class<?>[] groups() default {};

 

Class<? extends Payload>[] payload() default {};

 

}

Na powyższym listingu widzimy – poza samą deklaracją adnotacji – kilka podstawowych elementów, niezbędnych dla walidatora Bean Validation. W pierwszym wierszu widzimy adnotację @Constraint. Wskazuje ona na klasę, w której zawrzemy logikę swojej walidacji. Na powyższym przykładzie będzie to klasa AddressMinimumValidator. Kolejna adnotacja to @Target. Jej zadaniem jest określenie, gdzie będzie można umieszczać adnotację tworzonego przez nas walidatora (nad deklaracją klasy, parametrami metody, metodami itp.). W naszym przykładzie wskazaliśmy, że chcemy, by tworzony walidator stosowany mógł być w odniesieniu do pól klasy (FIELD). Kolejna adnotacja - @Retention określa, kiedy tworzona przez nas adnotacja walidatora ma być wykorzystywana. Ponieważ walidacja odbywa się w runtimie, taką właśnie wartość należało jej przypisać).

Kolejne trzy elementy niezbędne przy tworzeniu własnych walidatorów to zadeklarowanie trzech metod: message() (w której zdefiniować możemy domyślny komunikat błędu), groups() (która omówiona zostanie za chwilę) oraz payload() (znajdująca zastosowanie relatywnie najrzadziej).

 Aby stworzyć pełnoprawny, działający walidator należy – oprócz zadeklarowania interfejsu – utworzyć również klasę, w której zawarta zostanie logika walidacji, określająca w jakim przypadku dane można uznać za prawidłowe, w jakim zaś zakomunikować o błędzie. W opisywanym przypadku wskazaliśmy już, że logika walidacji zawarta zostanie w klasie AddressMinimumValidator. Klasa taka mogłaby wyglądać następująco:

public class AddressMinimumValidator implements ConstraintValidator<AddressMinimum, Address> {

 

@Override

public void initialize(AddressMinimum constraintAnnotation) {

ConstraintValidator.super.initialize(constraintAnnotation);

}

 

@Override

public boolean isValid(Address value, ConstraintValidatorContext constraintValidatorContext) {

if (null != value.getCountry() && "Polska".equalsIgnoreCase(value.getCountry())) {

return (null != value.getCity() && null != value.getApartmentNr());

} else {

return null != value.getCity();

}

}

}

Jak widać na powyższym listingu, klasa zawierająca logikę walidacji dla zdefiniowanej wcześniej adnotacji walidatora. Klasa taka musi przede wszystkim implementować interfejs ConstraintValidator. Interfejs ten parametryzowany jest dwoma typami. W pierwszej kolejności podajemy typ interfejsu będącego adnotacją walidatora (na przykładzie: jest to zdefiniowany przez nas poprzednio interfejs AddressMinimum. Drugi parametr wskazuje typ, który chcemy walidować. Na powyższym przykładzie jest to utworzona przez nas klasa Address, lecz może to być dowolny typ javowy lub utworzony przez użytkownika (w tym interfejs lub typ abstrakcyjny).

Klasa logiki walidatora implementować musi dwie metody: initialize() oraz isValid(). W pierwszej z nich w większości prostych przypadków wystarczy, że ograniczymy się do wywołania domyślnej metody implementowanego internejsu. W metodzie tej możemy także inicjalizować dodatkowe atrybuty interfejsu walidatora (obok obowiązkowych: message, groups oraz payload). W drugiej metodzie mamy dostęp do dwóch parametrów: pierwszy z nich (na przykładzie: Address) jest typu, który chcemy walidować. Sprawdzając jego właściwości z metody tej zwrócić musimy true (walidacja przechodzi poprawnie) lub false (walidacja się nie powiodła). Dodatkowo w metodzie isValid() dysponujemy obiektem ConstraintValidatorContext, czyli kontekstem walidacji (dzięki temu możemy np. „w locie” wyłączyć błąd walidacji, dodać nowy czy zmienić jego lokalizację). W przedstawionym przykładzie ograniczyliśmy się do sprawdzenia, czy państwo to Polska i zależnie od tego do zweryfikowania, czy adres zawiera określone, wymagane pola.

W poprzednim akapicie zawarta została informacja, że w metodzie initialize() inicjalizować można dodatkowe atrybuty interfejsu walidatora. Wyobraźmy sobie że adres korespondencyjny chcielibyśmy walidować inaczej niż adres zamieszkania. W takiej sytuacji w adnotacji walidatora dodać musimy definicję metody, która będzie równocześnie atrybutem walidatora:

@Documented

@Constraint(validatedBy = AddressMinimumValidator.class)

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

@Repeatable(AddressMinimum.List.class)

public @interface AddressMinimum {

 

String message() default "The address minimum is not filled";

 

Class<?>[] groups() default {};

 

Class<? extends Payload>[] payload() default {};

 

boolean correspondence();

 

@Documented

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

 

@interface List {

AddressMinimum[] value();

 

}

 

}

Na powyższym przykładzie jest to metoda correspondence() typu boolean, zwracająca informację, czy adres służy do korespondencji. Ponieważ jedną adnotację walidatora można co do zasady umieścić nad walidowanym typem jednokrotnie (a my będziemy chcieli umieścić ją dwa razy), konieczne było dodanie adnotacji @Repetable oraz dodanie interfejsu pozwalającego na powtarzanie tej samej adnotacji kilkukrotnie nad jednym polem (interfejs List na powyższym przykładzie).

public class AddressMinimumValidator implements ConstraintValidator<AddressMinimum, Address> {

 

boolean correspondence;

 

@Override

public void initialize(AddressMinimum constraintAnnotation) {

this.correspondence = constraintAnnotation.correspondence();

}

 

@Override

public boolean isValid(Address value, ConstraintValidatorContext constraintValidatorContext) {

if (correspondence) {

//walidacja jeśli korespondencyjny

} else {

//walidacja jeśli nie korespondencyjny

}

}

}

Jak widać, zasadnicza różnica względem wcześniejszego przykładu jest taka, że w klasie tworzymy zmienną typu boolean, zaś w metodzie initialize() przypisujemy jej wartość z adnotacji, którą w dalszej części klasy możemy wykorzystać w metodzie isValid().

W końcu, warto spojrzeć, jak taka walidacja wyglądać może od strony samej adnotacji walidatora:

@AddressMinimum(groups = Correspondence.class, correspondence = true)

@AddressMinimum(groups = Other.class, correspondence = false)

Address address;

Widzimy, że nad polem adres mieszczony został dwukrotnie walidator walidujący minimum adresowe. Zawiera on atrybut correspondence, którego możemy użyć w opisany wcześniej sposób. Widzimy także, w jaki sposób można uruchomić właściwą walidację: przypisując adnotację walidacji „korespondencyjnej” i „niekorespondencyjnej” do odrębnych grup. Grupą taką (na przykładzie: Correspondence oraz Other) może być dowolny typ (nawet pusty interfejs), służący jedynie rozróżnieniu. Mając zapis taki jak powyżej, który odczytać można: jeśli walidacja wywołana zostanie z grupy Correspondence, to waliduj jako adres korespondencyjny, jeśli zaś z grupy Other – jako niekorespondencyjny, możemy wywołać walidację w pożądany sposób. Zależnie od programu możemy jawnie przekazać grupę do metody wywołującej walidację lub też przekazać ją do atrybutu @Valiated wykorzystywanego często przy tworzeniu API.

Jak pokazały powyższe przykłady, tworzenie własnych walidatorów jest relatywnie proste i szybkie i stanowi dobrą alternatywę dla walidowania danych za pomocą metod w logice biznesowej.


Michał Karmelita

Z wykształcenia prawnik i informatyk. Zawodowo i z zamiłowania programista Java. W wolnym czasie stara się oddawać przyjemnościom i unikać przykrości. Zapalony podróżnik.


Wydrukuj