Bean Validation świetnie współpracuje ze Spring Bootem. Za jego pomocą możemy bardzo łatwo sprawdzać poprawność danych przesłanych do naszej aplikacji. Postaram się pokazać na przykładach jak to zrobić.
Wpis ten jest kontynuacją wpisu o Java Bean Validation, omawiałem w nim podstawowe wbudowane walidatory danych oraz jak wywołać walidację programatycznie. Cały wpis znajduje się tutaj: Java Bean Validation – sprawdzanie poprawności przesłanych danych.
Kod projektu użytego we wpisie znajduje się w repozytorium Gita. Możesz je znaleźć tutaj: https://github.com/mloza/spring-boot-bean-validation.
Tworzymy projekt
Zacznijmy od stworzenia prostego projektu i dodania zależności w Mavenie. W naszym pom.xml znajdą się zależności do Spring Boota, Java Validation API oraz do Hibernate Validator. Możemy również dodać Spring Boot Starter Test, aby testować nasze rozwiązania. Wygląda on następująco.
View this code snippet on GitHub.Po stworzeniu poma, do całości brakuje nam jeszcze tylko klasy głównej, która będzie nam uruchamiać projekt. Stwórzmy ją od razu, będzie ona miała następującą postać.
View this code snippet on GitHub.Mamy w tym momencie już gotowy projekt. Zobaczmy więc w jaki sposób możemy wykorzystać Bean Validation.
Walidacja danych wejściowych do kontrolera
Dane do kontrolera możemy przekazywać na kilka sposobów: przez url, query params lub body w zapytaniach POST. Wszystkie te sposoby wspierają sprawdzanie poprawności danych. W kolejnych akapitach omówimy sobie poszczególne możliwości.
Walidacja danych w URL i QueryParam
Najprostszym sposobem przekazania danych do kontrolera jest ich zaszycie w URL. Możemy je umieścić bezpośrednio w adresie (czyli np. http://blog.mloza.pl/site/1234 – 1234 będzie naszym parametrem) lub w query path (wtedy to będzie wyglądało następująco: http://blog.mloza.pl/site?id=1234). Zobaczmy, jak to może wyglądać w kodzie.
View this code snippet on GitHub.Jak możecie się domyślać, jest to zwykły kontroler z kilkoma dodatkowymi adnotacjami. Aby parametry były sprawdzane, przy kontrolerze musi być adnotacja @Validated (#1). Następnie przy deklaracji parametrów przyjmowanych przez metodę określamy, jakie musi spełnić warunki. W pierwszym przypadku mówimy, iż @PathVariable – zmienna przekazywana w url, musi być większa lub równa 10 oraz mniejsza, lub równa 20 (#2). W drugim przypadku mówimy, iż zmienna przekazywana w query o nazwie id musi mieć wartość, co najmniej 10. jeżeli te warunki nie zostaną spełnione, zwrócony zostanie błąd 500 i wyrzucony stacktrace na konsolę.
2020-11-11 17:16:45.529 ERROR 90294 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: validationInQuery.id: musi być równe lub większe od 10] with root cause javax.validation.ConstraintViolationException: validationInQuery.id: musi być równe lub większe od 10 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE] at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE] at pl.mloza.controller.ValidateController$$EnhancerBySpringCGLIB$$8fc85953.validationInQuery(<generated>) ~[classes/:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na] [....]Jak ładnie obsłużyć błędy z walidacji pokażę później.
Walidacja danych w POST body
Jeśli przesyłamy dane w POST, to zwykle nie są to już typy proste. Obiekty złożone tez możemy sprawdzać w podobny sposób. Zobaczmy przykład poniżej.
View this code snippet on GitHub.Jak widzisz, nasze body jest obiektem. Aby wymusić sprawdzenie, obiektu używamy adnotacji @Valid. Mówi ona Springowi, iż tak zaadnotowany obiekt musi być prawidłowy. Oznacza to tyle, iż sprawdzane są ograniczenia wewnątrz obiektu. Nasz obiekt może wyglądać jak poniżej.
View this code snippet on GitHub.W skrócie dodajemy adnotacje znane nam już z poprzednich przykładów do pól obiektu. Ważne! Jeśli nasza klasa posiada inne typy złożone, to muszą one być zaadnotowane przez @Valid, aby zostały sprawdzone.
Testowanie naszej walidacji
Teraz już tak łatwo nie wyślemy zapytania przez przeglądarkę. Najłatwiej będzie skonstruować test, który wyśle zapytanie i sprawdzi poprawność naszych reguł. Przykładowy test może wyglądać tak jak poniżej.
View this code snippet on GitHub.Po uruchomieniu testu w konsoli możemy zobaczyć ostrzeżenie, iż kontroler dostał nieprawidłowe dane ze szczegółowym opisem, co się nie zgadzało.
Obsługa błędów
Aktualnie jeżeli otrzymamy błąd walidacji, nie dostajemy żadnej informacji, co było nie tak. Można temu zaradzić, tworząc ControllerAdvice. Z jego pomocą powiemy Springowi, jak ma obsługiwać błędy w przypadku niepoprawnych danych, jak wyciągnąć dodatkowe informacje i jak je przekazać użytkownikowi. Może on wyglądać tak jak poniżej.
View this code snippet on GitHub.Klasa taka musi zostać zaadnotowana przez @ControllerAdvice (#1). Następnie dzięki adnotacji @ExceptionHandler (#2) na poziomie meotdy, mówimy jakie wyjątki chcemy obsługiwać. W naszym przypadku są to dwa typy wyjątków, więc potrzebujemy dwóch oddzielnych metod. MethodArgumentNotValid jest rzucany gdy mamy nieprawidłowe body, a ConstraintViolationException gdy przekazany do metody argument, na przykład przez URL lub QueryPath, jest nieprawidłowy.
Możesz zauważyć też, iż używam klas ValidationError i ValidationErrorResponse. Są to klasy, które trzeba sobie stworzyć, nie pochodzą z frameworka. Mogą one wyglądać jak poniżej.
View this code snippet on GitHub.Jeśli teraz uruchomimy nasz test, lub przejdziemy przeglądarką pod adres z nieprawidłowym parametrem, otrzymamy pełne informacje na temat błędu. Test nam zwróci następująco odpowiedź w response body.
{ "errors": [ { "fieldName": "email", "message": "must be a well-formed email address" }, { "fieldName": "age", "message": "You're too young" }, { "fieldName": "name", "message": "size must be between 2 and 10" }, { "fieldName": "zipCode", "message": "must match \"^[0-9]{2}-[0-9]{3}$\"" } ] }Pole message jest uzupełnione automatycznie przez domyślne wiadomości. Przy dodawaniu adnotacji możemy umieścić swoją wiadomość, która ma zostać zwrócona w przypadku błędu.
Walidacja danych przekazywanych do serwisu
Oprócz sprawdzania poprawności danych przekazanych do kontrolera możemy również sprawdzać, czy przekazujemy poprawne dane do serwisu.
View this code snippet on GitHub.Dla uproszczenia użyłem klasy PostBody wykorzystywanej już w kontrolerze. Aby walidacja w serwisie zadziałała, musimy dodać adnotację @Validated na poziomie klasy oraz @Valid przy parametrze, który ma zostać sprawdzony.
Możemy teraz dodać test, który sprawdzi, czy nasza walidacja działa.
View this code snippet on GitHub.Jak widzimy, w przypadku przesłania niepoprawnych danych zostanie wyrzucony wyjątek ConstraintViolationException.
Walidacja danych w JPA Entities
Ostatnim elementem, który chce poruszyć w artykule, jest walidacja danych zapisywanych w bazie danych. Jest to trochę późno na sprawdzanie danych, ponieważ może to oznaczać, iż cała nasza logika biznesowa wcześniej pracowała na niepoprawnych danych. Jednak w niektórych wypadkach może się przydać.
Zacznijmy od dodania zależności do Spring JPA oraz do bazy danych H2.
View this code snippet on GitHub.Teraz możemy stworzyć nasze entity z odpowiednimi adnotacjami.
View this code snippet on GitHub.Tutaj już nie musimy dodawać adnotacji @Validated, wystarczy dodać adnotacje przy polach. Następnie możemy stworzyć test, który nam sprawdzi czy walidacja działa.
View this code snippet on GitHub.Tym razem przy próbie zapisania dostaniemy wyjątek TransactionSystemException, pod którym kryje się RollbackException, a pod nim ConstraintViolationException.
Podsumowanie
Jak widzicie, można bardzo łatwo sprawdzać poprawność danych w systemie na różnych poziomach. Od kontrolera, przez serwisy po bazę danych. Wystarczy dodać kilka adnotacji i mamy gotową walidację. W kolejnym poście pokażę jak stworzyć swój własny walidator. Można go będzie użyć przy sprawdzaniu nietypowych danych, których nie można sprawdzić dzięki standardowych walidatorów.