162 votes

Spring MVC : Comment effectuer la validation ?

J'aimerais savoir quel est le moyen le plus propre et le plus efficace d'effectuer une validation de formulaire pour les entrées utilisateur. J'ai vu certains développeurs implémenter org.springframework.validation.Validator . Une question à ce sujet : J'ai vu qu'il valide une classe. La classe doit-elle être remplie manuellement avec les valeurs de la saisie de l'utilisateur, puis transmise au validateur ?

Je ne sais pas quelle est la meilleure façon de valider l'entrée de l'utilisateur. Je connais la méthode traditionnelle qui consiste à utiliser request.getParameter() puis en vérifiant manuellement les nulls mais je ne veux pas faire toute la validation dans mon Controller . Un bon conseil dans ce domaine serait très apprécié. Je n'utilise pas Hibernate dans cette application.

0 votes

334voto

Jerome Dalbert Points 3189

Avec Spring MVC, il existe trois façons différentes d'effectuer une validation : en utilisant une annotation, manuellement, ou un mélange des deux. Il n'y a pas une seule et unique "façon propre et meilleure" de valider, mais il y en a probablement une qui correspond mieux à votre projet/problème/contexte.

Prenons un utilisateur :

public class User {

    private String name;

    ...

}

Méthode 1 : Si vous avez Spring 3.x+ et une validation simple à effectuer, utilisez javax.validation.constraints (également connues sous le nom d'annotations JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Vous aurez besoin d'un fournisseur JSR-303 dans vos bibliothèques, comme par exemple Validateur Hibernate qui est l'implémentation de référence (cette bibliothèque n'a rien à voir avec les bases de données et le mapping relationnel, elle fait juste de la validation :-).

Dans votre contrôleur, vous auriez alors quelque chose comme :

@RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, @Valid @ModelAttribute("user") User user, BindingResult result){
    if (result.hasErrors()){
      // do something
    }
    else {
      // do something else
    }
}

Remarquez le @Valid : si l'utilisateur a un nom nul, result.hasErrors() sera vrai.

Méthode 2 : Si vous disposez d'une validation complexe (comme une logique de validation d'entreprise importante, une validation conditionnelle sur plusieurs champs, etc.) ou si, pour une raison quelconque, vous ne pouvez pas utiliser la méthode 1, utilisez la validation manuelle. Une bonne pratique consiste à séparer le code du contrôleur de la logique de validation. Ne créez pas votre (vos) classe(s) de validation à partir de rien, Spring fournit un outil pratique de org.springframework.validation.Validator (depuis Spring 2).

Disons que vous avez

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

et vous voulez faire une validation "complexe" du type : si l'âge de l'utilisateur est inférieur à 18 ans, responsibleUser ne doit pas être nul et l'âge de responsibleUser doit être supérieur à 21 ans.

Vous ferez quelque chose comme ceci

public class UserValidator implements Validator {

    @Override
    public boolean supports(Class clazz) {
      return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
      User user = (User) target;

      if(user.getName() == null) {
          errors.rejectValue("name", "your_error_code");
      }

      // do "complex" validation here

    }

}

Dans votre contrôleur, vous auriez alors :

@RequestMapping(value="/user", method=RequestMethod.POST)
    public createUser(Model model, @ModelAttribute("user") User user, BindingResult result){
        UserValidator userValidator = new UserValidator();
        userValidator.validate(user, result);

        if (result.hasErrors()){
          // do something
        }
        else {
          // do something else
        }
}

S'il y a des erreurs de validation, result.hasErrors() sera vrai.

Note : Vous pouvez également définir le validateur dans une méthode @InitBinder du contrôleur, avec "binder.setValidator(...)". (dans ce cas, une utilisation mixte des méthodes 1 et 2 ne serait pas possible, car vous remplacez le validateur par défaut). Ou vous pourriez l'instancier dans le constructeur par défaut du contrôleur. Ou avoir un @Component/@Service UserValidator que vous injectez (@Autowired) dans votre contrôleur : très utile, car la plupart des validateurs sont des singletons + le mocking des tests unitaires devient plus facile + votre validateur peut appeler d'autres composants Spring.

Méthode 3 : Pourquoi ne pas utiliser une combinaison des deux méthodes ? Valider les choses simples, comme l'attribut "name", avec des annotations (c'est rapide à faire, concis et plus lisible). Garder les validations lourdes pour les validateurs (quand cela prendrait des heures de coder des annotations de validation complexes personnalisées, ou simplement quand il n'est pas possible d'utiliser les annotations). C'est ce que j'ai fait dans le cadre d'un projet précédent, et cela a fonctionné à merveille, rapidement et facilement.

Avertissement : vous ne devez pas vous tromper gestion des validations para traitement des exceptions . Lire cet article pour savoir quand les utiliser.

Références :

0 votes

Pouvez-vous me dire ce que devrait contenir mon servlet.xml pour cette configuration. Je veux renvoyer les erreurs à la vue.

0 votes

@dev_darin Vous voulez dire la configuration pour la validation JSR-303 ?

0 votes

Oui validation JSR-303 je l'ai ré-acceptée désolé j'ai maintenant réalisé que vous ne pouvez accepter qu'une seule réponse. la réponse de steve.hanson était excellente aussi, j'essayais juste d'être juste envers deux personnes compétentes.

31voto

steve.hanson Points 1665

Il existe deux façons de valider les entrées utilisateur : les annotations et l'héritage de la classe Validator de Spring. Pour les cas simples, les annotations sont intéressantes. Si vous avez besoin de validations complexes (comme la validation inter-champs, par exemple le champ "vérifier l'adresse e-mail"), ou si votre modèle est validé à plusieurs endroits dans votre application avec des règles différentes, ou si vous n'avez pas la possibilité de modifier votre objet modèle en y plaçant des annotations, le validateur de Spring basé sur l'héritage est la solution. Je vais montrer des exemples des deux.

La partie validation proprement dite est la même, quel que soit le type de validation utilisé :

RequestMapping(value="fooPage", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("foo") Foo foo, BindingResult result, ModelMap m) {
    if(result.hasErrors()) {
        return "fooPage";
    }
    ...
    return "successPage";
}

Si vous utilisez des annotations, votre Foo pourrait ressembler :

public class Foo {

    @NotNull
    @Size(min = 1, max = 20)
    private String name;

    @NotNull
    @Min(1)
    @Max(110)
    private Integer age;

    // getters, setters
}

Les annotations ci-dessus sont javax.validation.constraints annotations. Vous pouvez également utiliser l'outil Hibernate org.hibernate.validator.constraints mais il ne semble pas que vous utilisiez Hibernate.

Alternativement, si vous implémentez le Validator de Spring, vous créerez une classe comme suit :

public class FooValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Foo.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

        Foo foo = (Foo) target;

        if(foo.getName() == null) {
            errors.rejectValue("name", "name[emptyMessage]");
        }
        else if(foo.getName().length() < 1 || foo.getName().length() > 20){
            errors.rejectValue("name", "name[invalidLength]");
        }

        if(foo.getAge() == null) {
            errors.rejectValue("age", "age[emptyMessage]");
        }
        else if(foo.getAge() < 1 || foo.getAge() > 110){
            errors.rejectValue("age", "age[invalidAge]");
        }
    }
}

Si vous utilisez le validateur ci-dessus, vous devez également lier le validateur au contrôleur Spring (ce qui n'est pas nécessaire si vous utilisez des annotations) :

@InitBinder("foo")
protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new FooValidator());
}

Voir aussi Documents sur le printemps .

J'espère que cela vous aidera.

0 votes

Lorsque j'utilise le validateur de Spring, dois-je définir le pojo à partir du contrôleur, puis le valider ?

0 votes

Je ne suis pas sûr de comprendre votre question. Si vous regardez l'extrait de code du contrôleur, Spring lie automatiquement le formulaire soumis à l'objet Foo dans la méthode du gestionnaire. Pouvez-vous préciser ?

0 votes

Ok, ce que je veux dire, c'est que lorsque l'utilisateur soumet les entrées utilisateur, le contrôleur reçoit la requête http, à partir de là, ce qui se passe, c'est que vous utilisez la requête.getParameter() pour obtenir tous les paramètres utilisateur, puis définir les valeurs dans le POJO, puis passer la classe à l'objet de validation. La classe de validation renverra les erreurs à la vue avec les erreurs trouvées, le cas échéant. Est-ce la façon dont cela se passe ?

13voto

michal.kreuzman Points 3214

Je voudrais prolonger la belle réponse de Jérôme Dalbert. J'ai trouvé très facile d'écrire vos propres validateurs d'annotations à la manière de la JSR-303. Vous n'êtes pas limité à la validation d'un seul champ. Vous pouvez créer votre propre annotation au niveau du type et avoir une validation complexe (voir les exemples ci-dessous). Je préfère cette méthode car je n'ai pas besoin de mélanger différents types de validation (Spring et JSR-303) comme le fait Jérôme. De plus, ces validateurs sont "Spring aware", vous pouvez donc utiliser @Inject/@Autowire dès le départ.

Exemple de validation d'un objet personnalisé :

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { YourCustomObjectValidator.class })
public @interface YourCustomObjectValid {

    String message() default "{YourCustomObjectValid.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

public class YourCustomObjectValidator implements ConstraintValidator<YourCustomObjectValid, YourCustomObject> {

    @Override
    public void initialize(YourCustomObjectValid constraintAnnotation) { }

    @Override
    public boolean isValid(YourCustomObject value, ConstraintValidatorContext context) {

        // Validate your complex logic 

        // Mark field with error
        ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
        cvb.addNode(someField).addConstraintViolation();

        return true;
    }
}

@YourCustomObjectValid
public YourCustomObject {
}

Exemple d'égalité des champs génériques :

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { FieldsEqualityValidator.class })
public @interface FieldsEquality {

    String message() default "{FieldsEquality.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * Name of the first field that will be compared.
     * 
     * @return name
     */
    String firstFieldName();

    /**
     * Name of the second field that will be compared.
     * 
     * @return name
     */
    String secondFieldName();

    @Target({ TYPE, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    public @interface List {
        FieldsEquality[] value();
    }
}

import java.lang.reflect.Field;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

public class FieldsEqualityValidator implements ConstraintValidator<FieldsEquality, Object> {

    private static final Logger log = LoggerFactory.getLogger(FieldsEqualityValidator.class);

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(FieldsEquality constraintAnnotation) {
        firstFieldName = constraintAnnotation.firstFieldName();
        secondFieldName = constraintAnnotation.secondFieldName();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null)
            return true;

        try {
            Class<?> clazz = value.getClass();

            Field firstField = ReflectionUtils.findField(clazz, firstFieldName);
            firstField.setAccessible(true);
            Object first = firstField.get(value);

            Field secondField = ReflectionUtils.findField(clazz, secondFieldName);
            secondField.setAccessible(true);
            Object second = secondField.get(value);

            if (first != null && second != null && !first.equals(second)) {
                    ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(firstFieldName).addConstraintViolation();

          ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(someField).addConstraintViolation(secondFieldName);

                return false;
            }
        } catch (Exception e) {
            log.error("Cannot validate fileds equality in '" + value + "'!", e);
            return false;
        }

        return true;
    }
}

@FieldsEquality(firstFieldName = "password", secondFieldName = "confirmPassword")
public class NewUserForm {

    private String password;

    private String confirmPassword;

}

1 votes

Je me demandais également si un contrôleur a généralement un validateur et j'ai vu où vous pouvez avoir des validateurs multiples, mais si vous avez un ensemble de validation défini pour un objet, l'opération que vous voulez effectuer sur l'objet est différente, par exemple une sauvegarde / mise à jour, pour une sauvegarde un certain ensemble de validation est nécessaire et une mise à jour un ensemble différent de validation est nécessaire. Existe-t-il un moyen de configurer la classe de validateurs pour qu'elle conserve toutes les validations basées sur l'opération ou devrez-vous utiliser plusieurs validateurs ?

1 votes

Vous pouvez également avoir une annotation de validation sur la méthode. Vous pouvez donc créer votre propre "validation de domaine" si je comprends bien votre question. Pour cela, vous devez spécifier ElementType.METHOD en @Target .

0 votes

Je comprends ce que vous dites. Pouvez-vous également m'indiquer un exemple pour une image plus claire ?

5voto

Ali Dehghani Points 23885

Si vous avez la même logique de traitement des erreurs pour différents gestionnaires de méthodes, vous vous retrouverez avec de nombreux gestionnaires avec le modèle de code suivant :

if (validation.hasErrors()) {
  // do error handling
}
else {
  // do the actual business logic
}

Supposons que vous créez des services RESTful et que vous voulez retourner 400 Bad Request ainsi que des messages d'erreur pour chaque cas d'erreur de validation. Ensuite, la partie de traitement des erreurs serait la même pour chaque point de terminaison REST qui nécessite une validation. Répéter la même logique dans chaque gestionnaire n'est pas si simple. SEC ish !

Une façon de résoudre ce problème est de laisser tomber l'élément immédiat BindingResult après chaque À valider haricot. Maintenant, votre gestionnaire serait comme ceci :

@RequestMapping(...)
public Something doStuff(@Valid Somebean bean) { 
    // do the actual business logic
    // Just the else part!
}

De cette façon, si le haricot lié n'était pas valide, un MethodArgumentNotValidException seront lancés par Spring. Vous pouvez définir un ControllerAdvice qui traite cette exception avec la même logique de traitement des erreurs :

@ControllerAdvice
public class ErrorHandlingControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public SomeErrorBean handleValidationError(MethodArgumentNotValidException ex) {
        // do error handling
        // Just the if part!
    }
}

Vous pouvez toujours examiner le sous-jacent BindingResult en utilisant getBindingResult méthode de MethodArgumentNotValidException .

1voto

Vicky Points 2492

Trouvez un exemple complet de Spring Mvc Validation

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.technicalkeeda.bean.Login;

public class LoginValidator implements Validator {
    public boolean supports(Class aClass) {
        return Login.class.equals(aClass);
    }

    public void validate(Object obj, Errors errors) {
        Login login = (Login) obj;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName",
                "username.required", "Required field");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userPassword",
                "userpassword.required", "Required field");
    }
}

public class LoginController extends SimpleFormController {
    private LoginService loginService;

    public LoginController() {
        setCommandClass(Login.class);
        setCommandName("login");
    }

    public void setLoginService(LoginService loginService) {
        this.loginService = loginService;
    }

    @Override
    protected ModelAndView onSubmit(Object command) throws Exception {
        Login login = (Login) command;
        loginService.add(login);
        return new ModelAndView("loginsucess", "login", login);
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X