Spring MVC Custom Validation

Spring MVC Custom Validation

1. Vue d'ensemble

En règle générale, lorsque nous devons valider les entrées de l'utilisateur, Spring MVC propose des validateurs prédéfinis standard.

Cependant, lorsque nous devons valider une entrée de type plus particulière,we have the possibility of creating our own, custom validation logic.

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

2. Installer

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


    org.hibernate
    hibernate-validator
    6.0.10.Final

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

Si nous utilisons Spring Boot, nous ne pouvons ajouter que lesspring-boot-starter-web, qui apporteront également la dépendancehibernate-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 noscustom validator – which checks phone numbers. Le numéro de téléphone doit être un numéro comportant plus de huit chiffres mais pas plus de 11 chiffres.

4. La nouvelle annotation

Créons un nouveau@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, lemessage() est le message d'erreur qui s'affiche dans l'interface utilisateur et le code supplémentaire est le code standard pour se conformer à la Normes de printemps.

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

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

ConstraintValidator ddéfinit la logique pour valider une contrainte donnée pour un objet donné. Les implémentations doivent respecter les restrictions suivantes:

  • l'objet doit être résolu en un type non paramétré

  • les paramètres génériques de l'objet doivent être des types génériques non lié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 être 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 pageJSP, et utilisons la méthodesubmitForm pour imposer la validation de notre numéro de téléphone.

7. La vue

Notre vue est une page JSP de base avec un formulaire comportant 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:


    
    
    
    

8. Des 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 prévu - tout fonctionne etwe’re seeing the error on the client side.

9. Validation de niveau de classe personnalisée

Une annotation de validation personnalisée peut également être définie au niveau de la classe pour valider plusieurs attributs 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éeFieldsValueMatch qui pourra être appliquée ultérieurement à une classe. L'annotation aura deux paramètresfield etfieldMatch 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-interfaceList pour définir plusieurs annotationsFieldsValueMatch sur une classe.

9.2. Création du validateur

Ensuite, nous devons ajouter la classeFieldsValueMatchValidator qui contiendra la logique de validation réelle:

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

La méthodeisValid() récupère les valeurs des deux champs et vérifie si elles sont égales.

9.3. Application de l'annotation

Créons une classe de modèleNewUserForm destinée aux données requises pour l'enregistrement d'un utilisateur, qui a deux attributsemail etpassword, ainsi que deux attributsverifyEmail etverifyPassword pour ressaisir les deux valeurs.

Étant donné que nous avons deux champs à vérifier par rapport à leurs champs correspondants, ajoutons deux annotations@FieldsValueMatch sur la classeNewUserForm, une pour les valeursemail et une pour les valeurspassword:

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

Pour valider le modèle dans Spring MVC, créons un contrôleur avec un mappage POST/user qui reçoit un objetNewUserForm annoté avec@Valid et vérifie s'il existe des erreurs de validation:

@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. Test de l'annotation

Pour vérifier notre annotation au niveau de la classe personnalisée, écrivons un testJUnit qui envoie les informations correspondantes au point de terminaison/user, puis vérifie que la réponse ne contient aucune erreur:

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 testJUnit 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. Sommaire

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

Comme toujours, vous pouvez trouver le code de l'articleover on Github.