Convalida personalizzata Spring MVC

1. Panoramica

In genere, quando è necessario convalidare l'input dell'utente, Spring MVC offre validatori predefiniti standard.

Tuttavia, quando dobbiamo convalidare un tipo più particolare di input, abbiamo la possibilità di creare la nostra logica di convalida personalizzata .

In questo articolo, faremo proprio questo: creeremo un validatore personalizzato per convalidare un modulo con un campo numero di telefono, quindi mostreremo un validatore personalizzato per più campi.

Questo articolo si concentra su Spring MVC. Il nostro articolo Validazione in Spring Boot descrive come eseguire convalide personalizzate in Spring Boot.

2. Configurazione

Per beneficiare dell'API, aggiungi la dipendenza al tuo file pom.xml :

 org.hibernate hibernate-validator 6.0.10.Final  

L'ultima versione della dipendenza può essere verificata qui.

Se stiamo usando Spring Boot, allora possiamo aggiungere solo spring-boot-starter-web, che introdurrà anche la dipendenza hibernate-validator .

3. Convalida personalizzata

La creazione di un validatore personalizzato implica il roll out della nostra annotazione e il suo utilizzo nel nostro modello per applicare le regole di convalida.

Quindi, creiamo il nostro validatore personalizzato, che controlla i numeri di telefono . Il numero di telefono deve essere un numero con più di otto cifre ma non più di 11 cifre.

4. La nuova annotazione

Creiamo una nuova interfaccia @ per definire la nostra annotazione:

@Documented @Constraint(validatedBy = ContactNumberValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface ContactNumberConstraint { String message() default "Invalid phone number"; Class[] groups() default {}; Class[] payload() default {}; }

Con l' annotazione @Constraint , abbiamo definito la classe che convaliderà il nostro campo, message () è il messaggio di errore che viene mostrato nell'interfaccia utente e il codice aggiuntivo è il codice più standard per essere conforme agli standard Spring.

5. Creazione di un validatore

Creiamo ora una classe validatore che imponga le regole della nostra validazione:

public class ContactNumberValidator implements ConstraintValidator { @Override public void initialize(ContactNumberConstraint contactNumber) { } @Override public boolean isValid(String contactField, ConstraintValidatorContext cxt) { return contactField != null && contactField.matches("[0-9]+") && (contactField.length() > 8) && (contactField.length() < 14); } }

La classe di convalida implementa l' interfaccia ConstraintValidator e deve implementare il metodo isValid ; è in questo metodo che abbiamo definito le nostre regole di convalida.

Naturalmente, qui seguiremo una semplice regola di convalida, per mostrare come funziona il validatore.

ConstraintValidator definisce la logica per convalidare un dato vincolo per un dato oggetto. Le implementazioni devono rispettare la seguente restrizione:

  • l'oggetto deve risolversi in un tipo non parametrizzato
  • i parametri generici dell'oggetto devono essere tipi di caratteri jolly illimitati

6. Applicazione dell'annotazione di convalida

Nel nostro caso, abbiamo creato una semplice classe con un campo per applicare le regole di validazione. Qui stiamo impostando il nostro campo annotato da convalidare:

@ContactNumberConstraint private String phone;

Abbiamo definito un campo stringa e lo abbiamo annotato con la nostra annotazione personalizzata @ContactNumberConstraint. Nel nostro controller abbiamo creato le nostre mappature e gestito l'eventuale errore:

@Controller public class ValidatedPhoneController { @GetMapping("/validatePhone") public String loadFormPage(Model m) { m.addAttribute("validatedPhone", new ValidatedPhone()); return "phoneHome"; } @PostMapping("/addValidatePhone") public String submitForm(@Valid ValidatedPhone validatedPhone, BindingResult result, Model m) { if(result.hasErrors()) { return "phoneHome"; } m.addAttribute("message", "Successfully saved phone: " + validatedPhone.toString()); return "phoneHome"; } }

Abbiamo definito questo semplice controller che ha una singola pagina JSP e utilizziamo il metodo submitForm per imporre la convalida del nostro numero di telefono.

7. La vista

La nostra vista è una pagina JSP di base con un modulo che ha un singolo campo. Quando l'utente invia il modulo, il campo viene convalidato dal nostro validatore personalizzato e reindirizza alla stessa pagina con il messaggio di convalida riuscita o non riuscita:

 Phone:      

8. Test

Proviamo ora il nostro controller e controlliamo se ci fornisce la risposta e la visualizzazione appropriate:

@Test public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){ this.mockMvc. perform(get("/validatePhone")).andExpect(view().name("phoneHome")); }

Inoltre, testiamo che il nostro campo sia convalidato, in base all'input dell'utente:

@Test public void givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() { this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone"). accept(MediaType.TEXT_HTML). param("phoneInput", "123")). andExpect(model().attributeHasFieldErrorCode( "validatedPhone","phone","ContactNumberConstraint")). andExpect(view().name("phoneHome")). andExpect(status().isOk()). andDo(print()); }

Nel test, stiamo fornendo a un utente l'input di "123" e, come ci aspettavamo, tutto funziona e stiamo vedendo l'errore sul lato client .

9. Convalida a livello di classe personalizzata

È inoltre possibile definire un'annotazione di convalida personalizzata a livello di classe per convalidare più di un attributo della classe.

Un caso d'uso comune per questo scenario è verificare se due campi di una classe hanno valori corrispondenti.

9.1. Creazione dell'annotazione

Aggiungiamo una nuova annotazione chiamata FieldsValueMatch che può essere successivamente applicata a una classe. L'annotazione avrà due parametri field e fieldMatch che rappresentano i nomi dei campi da confrontare:

@Constraint(validatedBy = FieldsValueMatchValidator.class) @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface FieldsValueMatch { String message() default "Fields values don't match!"; String field(); String fieldMatch(); @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @interface List { FieldsValueMatch[] value(); } }

Possiamo vedere che la nostra annotazione personalizzata contiene anche una sottointerfaccia List per la definizione di più annotazioni FieldsValueMatch su una classe.

9.2. Creazione del validatore

Successivamente, dobbiamo aggiungere la classe FieldsValueMatchValidator che conterrà la logica di convalida effettiva:

public class FieldsValueMatchValidator implements ConstraintValidator { private String field; private String fieldMatch; public void initialize(FieldsValueMatch constraintAnnotation) { this.field = constraintAnnotation.field(); this.fieldMatch = constraintAnnotation.fieldMatch(); } public boolean isValid(Object value, ConstraintValidatorContext context) { Object fieldValue = new BeanWrapperImpl(value) .getPropertyValue(field); Object fieldMatchValue = new BeanWrapperImpl(value) .getPropertyValue(fieldMatch); if (fieldValue != null) { return fieldValue.equals(fieldMatchValue); } else { return fieldMatchValue == null; } } }

Il metodo isValid () recupera i valori dei due campi e controlla se sono uguali.

9.3. Applicazione dell'annotazione

Creiamo una classe del modello NewUserForm destinata ai dati richiesti per la registrazione dell'utente, che ha due attributi email e password , insieme a due attributi verifyEmail e verifyPassword per reinserire i due valori.

Poiché abbiamo due campi da controllare rispetto ai campi corrispondenti corrispondenti, aggiungiamo due annotazioni @FieldsValueMatch sulla classe NewUserForm , una per i valori di posta elettronica e una per i valori di password :

@FieldsValueMatch.List({ @FieldsValueMatch( field = "password", fieldMatch = "verifyPassword", message = "Passwords do not match!" ), @FieldsValueMatch( field = "email", fieldMatch = "verifyEmail", message = "Email addresses do not match!" ) }) public class NewUserForm { private String email; private String verifyEmail; private String password; private String verifyPassword; // standard constructor, getters, setters }

Per convalidare il modello in Spring MVC, creiamo un controller con una mappatura POST / user che riceve un oggetto NewUserForm annotato con @Valid e verifica se ci sono errori di convalida:

@Controller public class NewUserController { @GetMapping("/user") public String loadFormPage(Model model) { model.addAttribute("newUserForm", new NewUserForm()); return "userHome"; } @PostMapping("/user") public String submitForm(@Valid NewUserForm newUserForm, BindingResult result, Model model) { if (result.hasErrors()) { return "userHome"; } model.addAttribute("message", "Valid form"); return "userHome"; } }

9.4. Verifica dell'annotazione

Per verificare la nostra annotazione a livello di classe personalizzata, scriviamo un test JUnit che invia le informazioni corrispondenti all'endpoint / user , quindi verifica che la risposta non contenga errori:

public class ClassValidationMvcTest { private MockMvc mockMvc; @Before public void setup(){ this.mockMvc = MockMvcBuilders .standaloneSetup(new NewUserController()).build(); } @Test public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders .post("/user") .accept(MediaType.TEXT_HTML). .param("email", "[email protected]") .param("verifyEmail", "[email protected]") .param("password", "pass") .param("verifyPassword", "pass")) .andExpect(model().errorCount(0)) .andExpect(status().isOk()); } }

Successivamente, aggiungiamo anche un test JUnit che invia informazioni non corrispondenti all'endpoint / user e asseriamo che il risultato conterrà due errori:

@Test public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders .post("/user") .accept(MediaType.TEXT_HTML) .param("email", "[email protected]") .param("verifyEmail", "[email protected]") .param("password", "pass") .param("verifyPassword", "passsss")) .andExpect(model().errorCount(2)) .andExpect(status().isOk()); }

10. Riepilogo

In questo rapido articolo, abbiamo mostrato come creare validatori personalizzati per verificare un campo o una classe e collegarli a Spring MVC.

Come sempre, puoi trovare il codice dall'articolo su Github.