Własne walidatory w Javie
W poprzednim wpisie pokazałem w jaki sposób walidować obiekty w Javie. Mamy dostępnych sporo domyślnych walidatorów, ale w jaki sposób napisać własny walidator ? Postaram się to pokazać na kilku prostych przykładach.
Wymagania
- Podstawowa znajomość tematu walidacji
- Wiedza z zakresu tworzenia projektu maven i dodawania zależności
- 15 minut wolnego czasu
Źródła
Źródła do tego wpisu, są dostępna na github: https://github.com/najavie/beanValidationExample
Konfiguracja
Tworzymy projekt maven, i dodajemy następujące zależności:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.el</groupId> <artifactId>javax.el-api</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>javax.el</artifactId> <version>2.2.6</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency> |
Implementacja
Enum Gender
Tworzymy enuma Gender:
1 2 3 4 5 6 7 |
package pl.najavie.validators; public enum Gender { MALE,FEMALE } |
Klasa Address
Tworzymy klasę Address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package pl.najavie; import javax.validation.constraints.NotNull; public class Address { @NotNull private String address; public Address(@NotNull String address) { this.address = address; } public String getAddress() { return address; } } |
Klasa Person
Tworzymy klasę Person ze standardowymi walidatorami:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
package pl.najavie; import org.hibernate.validator.constraints.pl.NIP; import org.hibernate.validator.constraints.pl.PESEL; import pl.najavie.validators.Gender; import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Past; import java.util.Date; public class Person { @NotBlank private String name; @NotBlank private String lastName; @Past private Date dateOfBirth; @NotNull @PESEL private String pesel; @NIP private String nip; @Valid private Address address; public String getName() { return name; } public String getLastName() { return lastName; } public Date getDateOfBirth() { return dateOfBirth; } public String getPesel() { return pesel; } public String getNip() { return nip; } public Address getAddress() { return address; } public Person(@NotBlank String name, @NotBlank String lastName, @Past Date dateOfBirth, @NotNull @PESEL String pesel, @NIP String nip, Address address) { this.name = name; this.lastName = lastName; this.dateOfBirth = dateOfBirth; this.pesel = pesel; this.nip = nip; this.address = address; } public Gender getGender() { if(pesel.length() != 11) { return null; } final int genderElement = Integer.parseInt(String.valueOf(pesel.charAt(9))); if(genderElement %2 ==0) { return Gender.FEMALE; } return Gender.MALE; } } |
Klasa Person ma dodatkową metodę, która pobiera płeć z pesela. Płeć leży na 10 elemencie, i jeśli 10 element jest parzysty, to mamy kobietę, a jeśli nieparzysty, to mamy faceta. Proste 🙂
Baza imion
Utworzymy sobie jeszcze jakieś małe repozytorium imion. W realnej aplikacji, to będzie zapewne baza danych, ale na nasze potrzeby wystarczy nam coś takiego:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package pl.najavie.validators; public class GenderName { private final String name; private final Gender gender; public String getName() { return name; } public Gender getGender() { return gender; } public GenderName(String name, Gender gender) { this.name = name; this.gender = gender; } } |
GenderName będzie reprezentowało informację o imieniu oraz płci.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package pl.najavie.validators; import com.google.common.collect.Lists; import java.util.List; public class NameRepository { private NameRepository() { super(); } public static final List<GenderName> names = Lists.newArrayList( new GenderName("Jan",Gender.MALE), new GenderName("Anna",Gender.FEMALE), new GenderName("Piotr",Gender.MALE), new GenderName("Brian",Gender.MALE), new GenderName("Jessica",Gender.FEMALE) ); } |
NameRepository zawiera listę kilku imion w połączeniu z płcią.
Adnotacja @CorrectName
Utworzymy teraz pierwszą własną adnotację – @CorrectName.
Adnotację chcemy nałożyć na imię, czyli pole String.
Chcemy też otrzymać jakiś komunikat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package pl.najavie.validators; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = CorrectNameValidator.class) public @interface CorrectName { String message() default "Nieprawidłowe imię"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
Struktura jest w miarę logiczna.
Musimy do adnotacji utworzyć też implementację walidatora, którego definiujemy poprzez:
1 |
@Constraint(validatedBy = CorrectNameValidator.class) |
No to zróbmy to!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package pl.najavie.validators; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class CorrectNameValidator implements ConstraintValidator<CorrectName, String> { public void initialize(CorrectName constraint) { } public boolean isValid(String name, ConstraintValidatorContext context) { return NameRepository.names.stream().map(GenderName::getName).anyMatch((element) -> element.equals(name)); } } |
Nasz walidator musi implementować ConstraintValidator sparametryzowany typem adnotacji oraz typem, jaki będzie walidował.
W metodzie initialize() możemy zainicjować jakieś dodatkowo nam potrzebne elementy. W tym przypadku, nic takiego nie potrzebujemy…
No i musimy zaimplementować metodę isValid(), która dokona już naszej walidacji imienia.
Walidacja ma polegać na tym, że akceptujemy tylko te pola, które mają imiona, które mamy w naszej „bazie danych”.
I to robimy:
1 |
return NameRepository.names.stream().map(GenderName::getName).anyMatch((element) -> element.equals(name)); |
Nic trudnego 🙂
Nakładamy teraz naszą walidację na pole name w Person:
1 2 3 |
@NotBlank @CorrectName private String name; |
I tyle 🙂
To zróbmy jeszcze kilka walidatorów…
Adnotacja @Adult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package pl.najavie.validators; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = AdultValidator.class) public @interface Adult { String message() default "Nie jest pełnoletni(a)"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
I jej implementacja:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package pl.najavie.validators; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; public class AdultValidator implements ConstraintValidator<Adult, Date> { public void initialize(Adult constraint) { } @Override public boolean isValid(Date date, ConstraintValidatorContext constraintValidatorContext) { final LocalDate from = LocalDate.from(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); final LocalDate now = LocalDate.now(); return now.minusYears(18).isAfter(from); } } |
Tutaj jak widzimy, nakładamy się na datę, i sprawdzamy, czy ktoś jest pełnoletni. Nie chcemy, aby 8mio letni Brajanek zrobił zakupy w naszym sklepie, prawda ?
Adnotacja @CorrectNameAndGender
A teraz trochę trudniej. Chcemy sprawdzić, czy imie pasuje do płci z pesela. Nie chcemy, aby Brajanek okazał się Jessicą, prawda?
Tutaj nie nałożymy adnotacji na pole, ale na całą klasę Person, aby mieć dostęp do wszystkich jej pól:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package pl.najavie.validators; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = CorrectPersonValidator.class) public @interface CorrectNameAndGender { String message() default "Imie nie pasuje do płci"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
I implementacja:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package pl.najavie.validators; import pl.najavie.Person; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class CorrectPersonValidator implements ConstraintValidator<CorrectNameAndGender, Person> { public void initialize(CorrectNameAndGender constraint) { } public boolean isValid(Person person, ConstraintValidatorContext context) { final Gender gender = person.getGender(); final String name = person.getName(); return NameRepository.names.stream().anyMatch(genderName -> genderName.getName().equals(name) && genderName.getGender().equals(gender)); } } |
Banalne, prawda ?
Jeszcze pozostał nam adres.
Adnotacja @Valid
Adnotacja @Valid, jest już zaimplementowana. Służy ona do tego, aby powiedzieć walidatorowi, że ma również inny obiekt zwalidować. Inaczej tego nie zrobi.
Czyli:
1 2 |
@Valid private Address address; |
Spowoduje, że będziemy walidowali pole address. W innym przypadku ten obiekt to nie byłby walidowany. Jest to częsty błąd i łatwo zapomnieć o tej adnotacji 🙂
No to napiszmy trochę testów, tak jak zasugerował mi w jednym komentarzu pewien czytelnik 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
package pl.najavie; import junit.framework.Assert; import junit.framework.TestCase; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; import java.util.Set; public class PersonTest extends TestCase { final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); final Validator validator = validatorFactory.getValidator(); public void testCorrectPerson() { Date dateOfBirth = Date.from(LocalDate.of(1992,8,29).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Person person = new Person("Jessica","Kowalska", dateOfBirth,"92082901340","7655702637",new Address("jasna 12, Gdynia")); final Set<ConstraintViolation<Person>> personConstraintViolations = validator.validate(person); Assert.assertTrue(personConstraintViolations.isEmpty()); } public void testIncorrectPersonAge() { Date dateOfBirth = Date.from(LocalDate.of(2013,8,29).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Person person = new Person("Brian","Kowalski", dateOfBirth,"13282905413","7655702637",new Address("jasna 12, Gdynia")); final Set<ConstraintViolation<Person>> personConstraintViolations = validator.validate(person); Assert.assertFalse(personConstraintViolations.isEmpty()); Assert.assertTrue(personConstraintViolations.size() == 1); Assert.assertTrue(personConstraintViolations.stream().anyMatch(element -> element.getMessage().equals("Nie jest pełnoletni(a)"))); } public void testIncorrectName() { Date dateOfBirth = Date.from(LocalDate.of(1992,8,29).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Person person = new Person("Karol","Kowalski", dateOfBirth,"87082913235","7655702637",new Address("jasna 12, Gdynia")); final Set<ConstraintViolation<Person>> personConstraintViolations = validator.validate(person); Assert.assertFalse(personConstraintViolations.isEmpty()); Assert.assertTrue(personConstraintViolations.size() == 2); Assert.assertTrue(personConstraintViolations.stream().anyMatch(element -> element.getMessage().equals("Nieprawidłowe imię"))); Assert.assertTrue(personConstraintViolations.stream().anyMatch(element -> element.getMessage().equals("Imie nie pasuje do płci"))); } public void testIncorrectAddress() { Date dateOfBirth = Date.from(LocalDate.of(1992,8,29).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); Person person = new Person("Jessica","Kowalska", dateOfBirth,"92082901340","7655702637",new Address(null)); final Set<ConstraintViolation<Person>> personConstraintViolations = validator.validate(person); Assert.assertFalse(personConstraintViolations.isEmpty()); Assert.assertTrue(personConstraintViolations.size() == 1); Assert.assertTrue(personConstraintViolations.stream().anyMatch(element -> element.getMessage().equals("must not be null"))); } } |
To tyle.
Jak widać, można w dość prosty sposób napisać własne walidatory, które sprawdzą nasz obiekt od góry do dołu. Zaoszczędzi nam to dużej ilości błędów później, bo nie będziemy mieli sytuacji, że jakieś pole jest nullem, a nigdy nie powinno być, że jakiś 11-to letni chłopczyk dokona zakupów, bo nie zwalidowaliśmy odpowiednio pesela, czy też imię będzie prawidłowe, a nie jakimś zlepkiem randomowych ciągów znaków.