Java Bean Validation – sprawdzanie poprawności przesłanych danych

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>
view raw pom-simple.xml hosted with ❤ by GitHub

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>
view raw pom.xml hosted with ❤ by GitHub

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;
@Email
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...
}
view raw User.java hosted with ❤ by GitHub

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;
}
}
view raw Pet.java hosted with ❤ by GitHub

Dodajemy do klasy User nowe pole i oznaczamy je stosownymi adnotacjami.

@Valid
@NotNull
private Pet pet;
view raw UserPet.java hosted with ❤ by GitHub

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;
view raw UserZip.java hosted with ❤ by GitHub

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.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *