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 e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *