Chcesz szybko sprawdzić, czy użytkownik przesłał poprawne dane do aplikacji? Z pomocą przyjdzie Ci Java Bean Validation! Używając tej biblioteki i kilku prostych adnotacji, jesteśmy w stanie opisać, jakich pól oczekujemy w obiekcie przekazanym do metody lub jakie ograniczenia powinny zostać narzucone parametrom przekazywanym do naszej metody.
Cała specyfikacja jest opisana jako JSR-380. Aktualnie najczęściej używaną i jedyną certyfikowaną implementacją jest Hibernate Validator. Tak, to ta sama firma, która stworzyła Hibernate do komunikacji z bazą danych, jednak Validator to oddzielna biblioteka i możesz ją używać niezależnie od bazy danych. Chociaż możemy z jej pomocą również sprawdzać poprawność encji, ale o tym w innym poście.
Zobaczmy, jak to wygląda w praktyce. Biblioteki możemy użyć samodzielnie i ręcznie wywołać sprawdzenie. Możemy jej użyć również w Springu i automatycznie sprawdzać dane przekazywane do naszego kontrolera. Lub jak już wspomniałem wcześniej, można ją użyć w połączeniu ze Spring JPA. W tym wpisie skupie się na pierwszej metodzie. Najłatwiej będzie mi to pokazać na przykładzie.
Cały kod projektu znajduje się w GitHubie pod adresem: https://github.com/mloza/java-bean-validation.
Tworzymy projekt
Pierwszym krokiem będzie stworzenie nowego projektu z odpowiednimi zależnościami. Jeśli nie chcecie korzystać ze Springa, będziecie potrzebować tylko hibernate-validator oraz el-expression-parser do przetwarzania adnotacji (ponieważ w wiadomościach o błędach jest wsparcie do interpolacji zmiennych).
<dependencies> | |
<dependency> | |
<groupId>javax.validation</groupId> | |
<artifactId>validation-api</artifactId> | |
<version>2.0.0.Final</version> | |
</dependency> | |
<dependency> | |
<groupId>org.hibernate.validator</groupId> | |
<artifactId>hibernate-validator</artifactId> | |
<version>6.1.5.Final</version> | |
</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> | |
</dependencies> |
Jeśli będziecie używać Springa, tak jak ja w dalszej części postu, możecie dodać go od razu w zależnościach i on dostarczy wszystkich wymaganych bibliotek. Wtedy cały pom.xml będzie wyglądał jak poniżej.
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>org.example</groupId> | |
<artifactId>bean-validation</artifactId> | |
<version>1.0-SNAPSHOT</version> | |
<dependencies> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web</artifactId> | |
<version>2.3.2.RELEASE</version> | |
</dependency> | |
<dependency> | |
<groupId>javax.validation</groupId> | |
<artifactId>validation-api</artifactId> | |
<version>2.0.0.Final</version> | |
</dependency> | |
<dependency> | |
<groupId>org.hibernate.validator</groupId> | |
<artifactId>hibernate-validator</artifactId> | |
<version>6.1.5.Final</version> | |
</dependency> | |
</dependencies> | |
<build> | |
<plugins> | |
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-compiler-plugin</artifactId> | |
<version>3.8.1</version> | |
<configuration> | |
<release>14</release> | |
</configuration> | |
</plugin> | |
</plugins> | |
</build> | |
</project> |
Walidacja programatyczna
Mając już wszystkie potrzebne zależności, mogę przejść do przykładów. Zacznę od stworzenia prostej klasy User, na której będę testował poprawność danych.
public class User { | |
@NotBlank | |
private String name; | |
@Min(value = 18, message = "You are to young to use our service") | |
@Max(value = 150, message = "People are not live that long") | |
private int age; | |
@AssertTrue | |
private boolean notARobot; | |
private String email; | |
@Size(min = 50, max = 255, message = "Create a text that is between 50 and 255 charters long") | |
private String aboutMe; | |
// getters, setters... | |
} |
Znaczenie poszczególnych adnotacji omówię za chwilę. Zobaczmy, jak można teraz uruchomić sprawdzenie na takim obiekcie.
public class ProgrammaticValidation { | |
public static void main(String[] args) { | |
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); // #1 | |
Validator validator = factory.getValidator(); | |
User user = new User(); // #2 | |
user.setName(""); | |
user.setAge(10); | |
user.setAboutMe("short"); | |
user.setEmail("notAnEmail"); | |
user.setNotARobot(false); | |
Set<ConstraintViolation<User>> violations = validator.validate(user); // #3 | |
System.out.println(violations); | |
User validUser = new User(); // #4 | |
validUser.setName("Michal"); | |
validUser.setAge(30); | |
validUser.setAboutMe("A little bit longer about me text. Text length should be in size constraints"); | |
validUser.setEmail("valid@email.com"); | |
validUser.setNotARobot(true); | |
Set<ConstraintViolation<User>> noViolations = validator.validate(validUser); // #5 | |
System.out.println(noViolations); | |
} | |
} |
Program tworzy najpierw instancje validatora za pośrednictwem fabryki (#1). Następnie tworzymy sobie obiekt, który będziemy sprawdzać (#2). W trzecim kroku wykonujemy sprawdzenie (#3). Validator automatycznie odczyta adnotacje na polach i zwróci nam listę błędów lub pustą listę jeśli wszystkie wartości pól są poprawne (#4 i #5). Wynikiem programu będzie wypisanie na konsoli informacji o błędach, jak to wygląda możesz zobaczyć poniżej.
[ConstraintViolationImpl{interpolatedMessage='must be true', propertyPath=notARobot, rootBeanClass=class pl.mloza.programmatic.User, messageTemplate='{javax.validation.constraints.AssertTrue.message}'}, ConstraintViolationImpl{interpolatedMessage='must be a well-formed email address', propertyPath=email, rootBeanClass=class pl.mloza.programmatic.User, messageTemplate='{javax.validation.constraints.Email.message}'}, ConstraintViolationImpl{interpolatedMessage='Create a text that is between 50 and 255 charters long', propertyPath=aboutMe, rootBeanClass=class pl.mloza.programmatic.User, messageTemplate='Create a text that is between 50 and 255 charters long'}, ConstraintViolationImpl{interpolatedMessage='You are to young to use our service', propertyPath=age, rootBeanClass=class pl.mloza.programmatic.User, messageTemplate='You are to young to use our service'}, ConstraintViolationImpl{interpolatedMessage='must not be blank', propertyPath=name, rootBeanClass=class pl.mloza.programmatic.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}] []
Użyte ograniczenia
Jak możecie zauważyć, klasa User to zwykłe POJO z kilkoma adnotacjami. Poszczególne adnotacje oznaczają:
- @NotBlank – Wartość nie może być pusta (musi zawierać przynajmniej jeden znak, który nie jest białym znakiem), nie może być też nullem,
- @Min i @Max – określa dolną i górną granicę wartości liczbowej,
- @AssertTrue – wartość zmiennej typu boolean musi być równa true,
- @Email – musi zawierać poprawny e-mail,
- @Size – określa jaką długość powinien mieć ciąg znaków. Działa też na wielkość tablic i kolekcji,
Pozostałe ograniczenia dostępne w Java Bean Validation
Poza wykorzystanymi adnotacjami, Java Bean Validation definiuje jeszcze:
- @DecimalMax i @DecimalMin – minimalna i maksymalna wartość ze wsparciem dla BigDecimal i BigInteger. Różni się od @Min i @Max tym, że wartość podajemy jako String.
- @Future, @FutureOrPresent, @Past, @PastOrPresent – Sprawdzanie daty
- @Digits – sprawdza, czy wartość posiada odpowiednią ilość cyfr w części całkowitej oraz po przecinku
- @NotBlank – podobnie jak @NotEmpty, z tą różnicą, że białe znaki też przejdą sprawdzenie
- @NotNull – wartość nie może być nullem
- @Negative, @NegativeOrZero, @Positive, @PositiveOrZero – sprawdza, czy wartość jest: ujemna, ujemna lub zerowa, dodatnia, dodatnia lub zerowa
- @Pattern – sprawdza, czy wartość pasuje do podanego wyrażenia regularnego
Ograniczenia dostarczane przez Hibernate Validator
Oprócz ograniczeń zdefiniowanych w Bean Validation, Hibernate Validator dodaje kilka swoich, które mogą być użyteczne:
- @CreditCardNumber – sprawdza poprawność numeru karty kredytowej
- @Currency – sprawdza walutę (działa na javax.money.MonetaryAmount)
- @DurationMax, @DurationMin – sprawdza, czy czas jest odpowiednio długi, można podać dni, godziny, minuty, sekundy… Działa z klasą java.time.Duration
- @EAN, @ISBN, @LuhnCheck, @Mod10Check , @Mod11Check– sprawdza poprawność numerów EAN, ISBN, poprawność według algorytmu Luhna lub mod 10/11
- @Length – podobnie jak @Size tylko, że działa tylko z ciągami znaków
- @Range – czy wartość leży w danym zakresie
- @SafeHtml – sprawdza, czy kod HTML nie zawiera potencjalnie groźnych znaczników, takich jak np.<script>. Możemy też zdefiniować dopuszczalne znaczniki i tagi, które przejdą sprawdzenie.
- @ScriptAssert – sprawdza poprawność za pomocą skryptu, może wykonać skrypty w językach, które są wspierane przez JSR223 (np. Jython, Groovy, JavaScript). Wymaga posiadania implementacji dodanej do zależności projektu,
- @UniqueElements – sprawdza, czy w kolekcji są tylko unikalne elementy, do porównania używa metody equals obiektów,
- @URL – sprawdza poprawność URLa,
Hibernate Validator posiada również kilka adnotacji specyficznych dla krajów. Dla Polski są to: @PESEL, @NIP, @REGON.
Sprawdzanie grafu obiektów
Dzięki Java Bean Validation możemy wykonywać sprawdzenie nie tylko na typach prostych, ale także na obiektach. Wystarczy dodać adnotację @Valid przy deklaracji pola i zostanie ono automatycznie sprawdzone. Załóżmy, że nasz użytkownik może mieć zwierzaka, który musi mieć jakieś imię.
public class Pet { | |
@NotBlank | |
private String name; | |
public Pet(@NotBlank String name) { | |
this.name = name; | |
} | |
public String getName() { | |
return name; | |
} | |
public Pet setName(String name) { | |
this.name = name; | |
return this; | |
} | |
} |
Dodajemy do klasy User nowe pole i oznaczamy je stosownymi adnotacjami.
@Valid | |
@NotNull | |
private Pet pet; |
I teraz przy sprawdzeniu klasy User od razu zostanie sprawdzona klasa Pet, która została dodana do użytkownika. Jeśli pole z imieniem pozostawimy puste, dostaniemy następujący błąd.
ConstraintViolationImpl{interpolatedMessage='must not be blank', propertyPath=pet.name, rootBeanClass=class pl.mloza.programmatic.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
Adnotowanie typów generycznych
Aby dodać ograniczenia do typów, które siedzą w Mapach, Listach czy Optionalach, możemy dodać adnotacje do typów generycznych. Przykładowo, chcemy, aby użytkownik mógł opcjonalnie podać swój kod pocztowy. Do jego sprawdzenia użyjemy adnotacji @Pattern.
private Optional<@Pattern(regexp = "\\d{2}-\\d{3}") String> zipCode; |
W mapie możemy adnotować zarówno klucz jak i wartość elementu.
Podsumowanie
I to już koniec podstawowych wiadomości o Validatorach. W kolejnych wpisach pokażę jak możemy używać Validacji razem ze Springiem oraz jak tworzyć własne Validatory.