Spring MVC: Comment effectuer la validation?

156

Je voudrais savoir quelle est la manière la plus propre et la meilleure pour effectuer la validation de formulaire des entrées utilisateur. J'ai vu certains développeurs implémenter org.springframework.validation.Validator. Une question à ce sujet: je l'ai vu valider une classe. La classe doit-elle être remplie manuellement avec les valeurs de l'entrée utilisateur, puis transmise au validateur?

Je suis confus quant à la manière la plus propre et la meilleure de valider l'entrée de l'utilisateur. Je connais la méthode traditionnelle d'utilisation request.getParameter()puis de vérification manuelle nulls, mais je ne veux pas faire toute la validation dans mon Controller. Quelques bons conseils dans ce domaine seront grandement appréciés. Je n'utilise pas Hibernate dans cette application.

devdar
la source

Réponses:

322

Avec Spring MVC, il existe 3 façons différentes d'effectuer la validation: en utilisant l'annotation, manuellement ou un mélange des deux. Il n'y a pas de «moyen le plus propre et le meilleur» de valider, mais il y en a probablement un qui correspond mieux à votre projet / problème / contexte.

Ayons un utilisateur:

public class User {

    private String name;

    ...

}

Méthode 1: Si vous avez Spring 3.x + et une validation simple à faire, utilisez des javax.validation.constraintsannotations (également appelées annotations JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

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

Ensuite, dans votre contrôleur, vous auriez 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
    }
}

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

Méthode 2: Si vous avez une validation complexe (comme la logique de validation des grandes entreprises, la validation conditionnelle sur plusieurs champs, etc.), ou pour une raison quelconque, vous ne pouvez pas utiliser la méthode 1, utilisez la validation manuelle. Il est recommandé de séparer le code du contrôleur de la logique de validation. Ne créez pas vos classes de validation à partir de zéro, Spring fournit une org.springframework.validation.Validatorinterface pratique (depuis Spring 2).

Alors disons que tu as

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

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

Tu feras quelque chose comme ça

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

    }

}

Ensuite, dans votre contrôleur, vous auriez:

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

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

Méthode 3: Pourquoi ne pas utiliser une combinaison des deux méthodes? Validez les trucs simples, comme l'attribut "nom", avec des annotations (c'est rapide à faire, concis et plus lisible). Conservez les validations lourdes pour les validateurs (lorsqu'il faudrait des heures pour coder des annotations de validation complexes personnalisées, ou juste lorsqu'il n'est pas possible d'utiliser des annotations). Je l'ai fait sur un ancien projet, cela a fonctionné comme un charme, rapide et facile.

Attention: il ne faut pas confondre la gestion de la validation avec la gestion des exceptions . Lisez cet article pour savoir quand les utiliser.

Références :

Jerome Dalbert
la source
pouvez-vous me dire ce que mon servlet.xml devrait avoir pour cette configuration. Je veux renvoyer les erreurs à la vue
devdar
@dev_darin Vous voulez dire config pour la validation JSR-303?
Jerome Dalbert
2
@dev_marin Pour la validation, dans Spring 3.x +, il n'y a rien de spécial dans "servlet.xml" ou "[servlet-name] -servlet.xml. Vous avez juste besoin du jar hibernate-validator dans vos bibliothèques de projet (ou via Maven). C'est tout, cela devrait fonctionner alors. Attention si vous utilisez la méthode 3: par défaut, chaque contrôleur a accès à un validateur JSR-303, donc veillez à ne pas le remplacer par "setValidator". Si vous souhaitez ajouter un validateur personnalisé sur top, instanciez-le et utilisez-le, ou injectez-le (s'il s'agit d'un composant Spring). Si vous rencontrez toujours des problèmes après avoir vérifié google et Spring doc, vous devriez poster une nouvelle question.
Jerome Dalbert
2
Pour l'utilisation mixte des méthodes 1 et 2, il existe un moyen d'utiliser @InitBinder. Au lieu de "binder.setValidator (...)", vous pouvez utiliser "binder.addValidators (...)"
jasonfungsing
1
Corrigez-moi si je me trompe, mais vous pouvez mélanger la validation via les annotations JSR-303 (méthode 1) et la validation personnalisée (méthode 2) lorsque vous utilisez l'annotation @InitBinder. Utilisez simplement binder.addValidators (userValidator) au lieu de binder.setValidator (userValidator) et les deux méthodes de validation prendront effet.
SebastianRiemer
31

Il existe deux façons de valider les entrées utilisateur: les annotations et en héritant de la classe Validator de Spring. Pour les cas simples, les annotations sont agréables. Si vous avez besoin de validations complexes (comme la validation croisée, 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 de modèle en plaçant des annotations dessus, le validateur basé sur l'héritage de Spring est la voie à suivre. Je vais montrer des exemples des deux.

La partie de validation réelle est la même quel que soit le type de validation que vous utilisez:

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 Fooclasse 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 des javax.validation.constraintsannotations. Vous pouvez également utiliser Hibernate org.hibernate.validator.constraints, mais il ne semble pas que vous utilisiez Hibernate.

Alternativement, si vous implémentez Spring's Validator, 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 (pas nécessaire si vous utilisez des annotations):

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

Consultez également la documentation Spring .

J'espère que cela pourra aider.

stephen.hanson
la source
lorsque j'utilise Spring's Validator, dois-je définir le pojo à partir du contrôleur, puis le valider?
devdar
Je ne suis pas sûr de comprendre votre question. Si vous voyez l'extrait de code du contrôleur, Spring lie automatiquement le formulaire soumis au Fooparamètre dans la méthode du gestionnaire. Pouvez-vous clarifier?
stephen.hanson
ok ce que je dis, c'est que lorsque l'utilisateur soumet les entrées de l'utilisateur, le contrôleur obtient la requête http, à partir de là, ce qui se passe, c'est que vous utilisez le request.getParameter () pour obtenir tous les paramètres utilisateur, puis définissez les valeurs dans le POJO puis passez la classe à l'objet de validation. La classe de validation renverra les erreurs à la vue avec les erreurs, le cas échéant. Est-ce ainsi que cela se passe?
devdar
1
Cela se passerait comme ça mais il existe un moyen plus simple ... Si vous utilisez JSP et une soumission <form: form commandName = "user">, les données sont automatiquement placées dans l'utilisateur @ModelAttribute ("user") dans le contrôleur méthode. Voir le doc: static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert
+1 parce que c'est le premier exemple que j'ai trouvé qui utilise @ModelAttribute; sans cela, aucun des tutoriels que j'ai trouvé ne fonctionne.
Riccardo Cossu
12

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 JSR-303. Vous n'êtes pas limité à avoir une validation "un 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 font Jérôme. De plus, ces validateurs sont "sensibles au printemps", vous pouvez donc utiliser @ Inject / @ Autowire hors de la boîte.

Exemple de validation d'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;

}
michal.kreuzman
la source
1
Je me demandais également qu'un contrôleur a généralement un validateur et j'ai vu où vous pouvez avoir plusieurs validateurs, mais si vous avez un ensemble de validation défini pour un objet, l'opération que vous souhaitez effectuer sur l'objet est différente, par exemple une sauvegarde / mise à jour pour un objet. enregistrer un certain ensemble de validation est nécessaire et une mise à jour d'un ensemble différent de validation est nécessaire. Existe-t-il un moyen de configurer la classe du validateur pour contenir toute la validation en fonction de l'opération ou devrez-vous utiliser plusieurs validateurs?
devdar
1
Vous pouvez également avoir une validation d'annotation sur la méthode. Vous pouvez donc créer votre propre "validation de domaine" si je comprends votre question. Pour cela, vous devez spécifier ElementType.METHODdans @Target.
michal.kreuzman
Je comprends ce que vous dites, pouvez-vous également me montrer un exemple pour une image plus claire.
devdar
4

Si vous avez la même logique de gestion 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 souhaitez renvoyer 400 Bad Requestdes messages d'erreur pour chaque cas d'erreur de validation. Ensuite, la partie de gestion des erreurs serait la même pour chaque point de terminaison REST qui nécessite une validation. Répéter cette même logique dans chaque gestionnaire n'est pas si DRY ish!

Une façon de résoudre ce problème consiste à supprimer immédiatement BindingResultaprès chaque bean à valider . 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 bean lié n'était pas valide, un MethodArgumentNotValidExceptionsera lancé par Spring. Vous pouvez définir un ControllerAdvicequi gère cette exception avec la même logique de gestion 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 en BindingResultutilisant la getBindingResultméthode de MethodArgumentNotValidException.

Ali Dehghani
la source
1

Trouver un exemple complet de validation Spring Mvc

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);
    }
}
Vicky
la source
0

Mettez ce bean dans votre classe de configuration.

 @Bean
  public Validator localValidatorFactoryBean() {
    return new LocalValidatorFactoryBean();
  }

et ensuite vous pouvez utiliser

 <T> BindingResult validate(T t) {
    DataBinder binder = new DataBinder(t);
    binder.setValidator(validator);
    binder.validate();
    return binder.getBindingResult();
}

pour valider un bean manuellement. Ensuite, vous obtiendrez tous les résultats dans BindingResult et vous pourrez récupérer à partir de là.

Praveen Jain
la source