Validation personnalisée Spring MVC

1. Vue d'ensemble

Généralement, lorsque nous devons valider les entrées utilisateur, Spring MVC propose des validateurs prédéfinis standard.

Cependant, lorsque nous devons valider un type d'entrée plus particulier, nous avons la possibilité de créer notre propre logique de validation personnalisée .

Dans cet article, c'est exactement ce que nous ferons: nous créerons un validateur personnalisé pour valider un formulaire avec un champ de numéro de téléphone, puis afficherons un validateur personnalisé pour plusieurs champs.

Cet article se concentre sur Spring MVC. Notre article Validation dans Spring Boot décrit comment effectuer des validations personnalisées dans Spring Boot.

2. Configuration

Pour bénéficier de l'API, ajoutez la dépendance à votre fichier pom.xml :

 org.hibernate hibernate-validator 6.0.10.Final  

La dernière version de la dépendance peut être vérifiée ici.

Si nous utilisons Spring Boot, nous ne pouvons ajouter que le spring-boot-starter-web, qui apportera également la dépendance hibernate-validator .

3. Validation personnalisée

La création d'un validateur personnalisé nous oblige à déployer notre propre annotation et à l'utiliser dans notre modèle pour appliquer les règles de validation.

Alors, créons notre validateur personnalisé - qui vérifie les numéros de téléphone . Le numéro de téléphone doit être un numéro de plus de huit chiffres mais pas plus de 11 chiffres.

4. La nouvelle annotation

Créons une nouvelle @interface pour définir notre annotation:

@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 {}; }

Avec l' annotation @Constraint , nous avons défini la classe qui va valider notre champ, le message () est le message d'erreur affiché dans l'interface utilisateur et le code supplémentaire est le code standard le plus conforme aux normes Spring.

5. Création d'un validateur

Créons maintenant une classe de validateur qui applique les règles de notre validation:

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 de validation implémente l' interface ConstraintValidator et doit implémenter la méthode isValid ; c'est dans cette méthode que nous avons défini nos règles de validation.

Naturellement, nous allons avec une règle de validation simple ici, pour montrer comment fonctionne le validateur.

ConstraintValidator définit la logique pour valider une contrainte donnée pour un objet donné. Les implémentations doivent respecter la restriction suivante:

  • l'objet doit se résoudre en un type non paramétré
  • les paramètres génériques de l'objet doivent être des types génériques illimités

6. Application de l'annotation de validation

Dans notre cas, nous avons créé une classe simple avec un champ pour appliquer les règles de validation. Ici, nous configurons notre champ annoté pour qu'il soit validé:

@ContactNumberConstraint private String phone;

Nous avons défini un champ de chaîne et l'avons annoté avec notre annotation personnalisée @ContactNumberConstraint. Dans notre contrôleur, nous avons créé nos mappages et géré l'erreur le cas échéant:

@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"; } }

Nous avons défini ce contrôleur simple qui a une seule page JSP et utilisons la méthode submitForm pour imposer la validation de notre numéro de téléphone.

7. La vue

Notre point de vue est une page JSP de base avec un formulaire qui a un seul champ. Lorsque l'utilisateur soumet le formulaire, le champ est validé par notre validateur personnalisé et redirige vers la même page avec le message de validation réussie ou échouée:

 Phone:      

8. Tests

Testons maintenant notre contrôleur et vérifions s'il nous donne la réponse et la vue appropriées:

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

Testons également que notre champ est validé, en fonction de l'entrée de l'utilisateur:

@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()); }

Dans le test, nous fournissons à un utilisateur l'entrée «123» et - comme nous nous y attendions - tout fonctionne et nous voyons l'erreur du côté client .

9. Validation du niveau de classe personnalisé

Une annotation de validation personnalisée peut également être définie au niveau de la classe pour valider plus d'un attribut de la classe.

Un cas d'utilisation courant pour ce scénario consiste à vérifier si deux champs d'une classe ont des valeurs correspondantes.

9.1. Création de l'annotation

Ajoutons une nouvelle annotation appelée FieldsValueMatch qui peut être appliquée ultérieurement à une classe. L'annotation aura deux paramètres field et fieldMatch qui représentent les noms des champs à comparer:

@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(); } }

Nous pouvons voir que notre annotation personnalisée contient également une sous-interface List pour définir plusieurs annotations FieldsValueMatch sur une classe.

9.2. Création du validateur

Next, we need to add the FieldsValueMatchValidator class that will contain the actual validation logic:

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; } } }

The isValid() method retrieves the values of the two fields and checks if they are equal.

9.3. Applying the Annotation

Let's create a NewUserForm model class intended for data required for user registration, that has two email and password attributes, along with two verifyEmail and verifyPassword attributes to re-enter the two values.

Since we have two fields to check against their corresponding matching fields, let's add two @FieldsValueMatch annotations on the NewUserForm class, one for email values, and one for password values:

@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 }

To validate the model in Spring MVC, let's create a controller with a /user POST mapping that receives a NewUserForm object annotated with @Valid and verifies whether there are any validation errors:

@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. Testing the Annotation

To verify our custom class-level annotation, let's write a JUnit test that sends matching information to the /user endpoint, then verifies that the response contains no errors:

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()); } }

Ensuite, ajoutons également un test JUnit qui envoie des informations non correspondantes au point de terminaison / user et affirmons que le résultat contiendra deux erreurs:

@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. Résumé

Dans cet article rapide, nous avons montré comment créer des validateurs personnalisés pour vérifier un champ ou une classe et les câbler dans Spring MVC.

Comme toujours, vous pouvez trouver le code de l'article sur Github.