Czasem zachodzi potrzeba sprawdzenia danych, z którymi domyślne walidatory sobie nie poradzą. Możemy wtedy stworzyć sobie własny walidator, który sprawdzi nam poprawność danych. Zobaczmy jak to można zrobić.
Post jest kontynuacją serii o Java Bean Validation. W poprzednich częściach pisałem, jak można programatycznie sprawdzać poprawność danych (Java Bean Validation – sprawdzanie poprawności przesłanych danych) oraz jak to działa w połączeniu ze springiem (Java Bean Validation + Spring Boot – sprawdzanie poprawności danych w Spring Boocie). Kod źródłowy do wszystkich wpisów znajduje się w GitHubie, pod adresem: https://github.com/mloza/spring-boot-bean-validation.
Dla potrzeb przykładu załóżmy, że chcemy sprawdzać poprawność kodu pocztowego. Możemy to zrobić prostym RegExem, ale używamy tego na tyle często, że chcemy mieć osobny walidator.
Krok I: Adnotacja
Zacznijmy od stworzenia adnotacji, którą będziemy używać do oznaczania pola, które powinno zostać sprawdzone przez nasz niestandardowy walidator danych. Może ona wyglądać następująco:
@Target({FIELD}) | |
@Retention(RUNTIME) | |
@Constraint(validatedBy = ZipCodeValidator.class). // #1 | |
@Documented | |
public @interface ZipCode { | |
String message() default "Podany kod pocztowy jest nieprawidÅowy"; // #2 | |
Class<?>[] groups() default {}; // #3 | |
Class<? extends Payload>[] payload() default {}; // #4 | |
} |
Naszą adnotację adnotujemy @Constraint (#1) i podajemy w validatedBy nazwę naszej klasy, która będzie dokonywała sprawdzenia (znajduje się poniżej). Następnie definiujemy, jaką wiadomość ma zwracać, gdy walidacja się nie powiedzie (#2). groups definiuje nam grupy walidacji, tego jeszcze nie omawiałem, możliwe, że pojawi się o tym oddzielny post, na razie możemy to pominąć. payload również nie omawiałem i na razie je pomińmy. (Jeśli chcecie o tym przeczytać więcej, dajcie znać w komentarzach)
Krok II: Walidator
Jeśli już mamy naszą adnotację to teraz możemy się zająć kodem do sprawdzania poprawności danych. Może on wyglądać następująco.
public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> { | |
@Override | |
public boolean isValid(String value, ConstraintValidatorContext context) { | |
Pattern pattern = Pattern.compile("^[0-9]{2}-[0-9]{3}$"); | |
return pattern.matcher(value).matches(); | |
} | |
} |
Nasz strażnik poprawności danych jest bardzo prosty. Klasa musi dziedziczyć po ConstraintValidator, który przyjmuje dwa parametry jako typy: pierwszy to typ adnotacji, drugi to typ danych, do których może być stosowany. W naszym przypadku podajemy adnotację ZipCode oraz typ String, czyli pola zawierające łańcuch znaków będą mogły być adnotowane.
Krok III: Użycie
Mamy już wszytko czego potrzebowaliśmy, aby przeprowadzić niestandardową walidację. Teraz możemy jej użyć. Zacznijmy od stworzenia klasy z polem zawierającym kod pocztowy.
public class RequestWithZipCode { | |
@ZipCode // #1 | |
private String zipCode; | |
// geter, seter, konstruktory... | |
} |
Bardzo prosta klasa zawierająca tylko jedno pole z naszą adnotacją @ZipCode.
Potrzebujemy ją gdzieś użyć. Stwórzymy więc kontroler, który przyjmie tę klasę jako body zapytania.
@RestController | |
public class CustomValidatorController { | |
@PostMapping("/zipCode") | |
@ResponseBody | |
public String zipCodeBody( | |
@RequestBody @Valid RequestWithZipCode body // #1 | |
) { | |
return "OK! Everything is valid"; | |
} | |
} |
Jak widzicie, nic się nie zmieniło w porównaniu do dotychczasowych przykładów z poprzednich postów. Znów mamy adnotację @Valid, która mówi Springowi, że to pole musi zostać sprawdzone. Zobaczmy czy tak się stanie.
Krok IV: Test
Aby szybko sprawdzić, czy nasza adnotacja działa, możemy stworzyć sobie test. Stworze tylko dwa przypadki testowe, aby sprawdzić, czy coś się w ogóle stanie. Cały test poniżej. Zawiera on więcej kodu niż wszystko, co do tej pory widziałeś razem wzięte 😀
@WebMvcTest | |
public class CustomValidatorControllerTest { | |
@Autowired | |
private MockMvc mvc; | |
@Autowired | |
private ObjectMapper objectMapper; | |
@Test | |
public void shouldReturnErrorWhenInputIsInvalid() throws Exception { | |
mvc.perform(post("/zipCode") | |
.contentType(MediaType.APPLICATION_JSON) | |
.content(invalidBody())) | |
.andDo(mvcResult -> System.out.println(mvcResult.getResponse().getContentAsString())) | |
.andExpect(status().isBadRequest()); | |
} | |
@Test | |
public void shouldReturnOkWhenInputIsValid() throws Exception { | |
mvc.perform(post("/zipCode") | |
.contentType(MediaType.APPLICATION_JSON) | |
.content(validBody())) | |
.andDo(mvcResult -> System.out.println(mvcResult.getResponse().getContentAsString())) | |
.andExpect(status().is2xxSuccessful()); | |
} | |
private String invalidBody() throws JsonProcessingException { | |
return objectMapper.writeValueAsString( | |
new RequestWithZipCode("1-21")); | |
} | |
private String validBody() throws JsonProcessingException { | |
return objectMapper.writeValueAsString( | |
new RequestWithZipCode("31-521")); | |
} | |
} |
Po odpaleniu testu wszytko powinno być zielone. Na konsolce powinny być wiadomości podobne do tych:
{"errors":[{"fieldName":"zipCode","message":"Podany kod pocztowy jest nieprawidłowy"}]} OK! Everything is valid
Pierwsza linia jest zwracana przez test przesyłający niepoprawne dane. Druga gdy dane są poprawne. Wygląda więc, że nasz walidator działa!
Podsumowanie
Widziałeś już jak w bardzo prosty sposób można stworzyć sobie niestandardowy walidator danych. Jest to bardzo łatwe i może nam zaoszczędzić wiele czasu z kombinowaniem używając standardowych ograniczeń, lub poprawić czytelność jeśli zamienimy bardzo długi regex na coś takiego.