web-dev-qa-db-fra.com

Spring MVC: Comment effectuer la validation?

J'aimerais savoir quel est le moyen le plus propre et le plus efficace de valider le formulaire des entrées de l'utilisateur. J'ai vu des développeurs implémenter org.springframework.validation.Validator . Une question à ce sujet: je l'ai vu valider un cours. La classe doit-elle être renseignée manuellement avec les valeurs de l'entrée utilisateur, puis transmise au validateur?

Je suis confus quant au moyen le plus propre et le plus efficace de valider les entrées de l'utilisateur. Je connais la méthode traditionnelle consistant à utiliser request.getParameter(), puis à rechercher manuellement nulls, mais je ne souhaite pas effectuer toute la validation dans ma Controller. Quelques bons conseils sur ce sujet seront grandement appréciés. Je n'utilise pas Hibernate dans cette application.

141
devdar

Avec Spring MVC, il existe 3 façons différentes de procéder à la validation: utiliser une annotation, manuellement ou combiner les deux. Il n’existe pas de méthode unique "la plus propre et la meilleure" pour valider, mais il en existe probablement une 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 qu'une validation simple est nécessaire, utilisez les annotations javax.validation.constraints (é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 valider :-).

Ensuite, dans votre contrôleur, vous aurez 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 null, result.hasErrors () sera true.

Méthode 2: Si la validation est complexe (logique de validation pour les grandes entreprises, validation conditionnelle sur plusieurs champs, etc.) ou si 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 votre classe de validation à partir de zéro, Spring fournit une interface org.springframework.validation.Validator très pratique (depuis Spring 2).

Alors disons que vous avez

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

et vous voulez effectuer une validation "complexe" comme par exemple: si l'utilisateur a moins de 18 ans, responsableUser ne doit pas être nul et responsableUser doit avoir plus de 21 ans.

Tu vas faire 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 aurez:

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

Remarque: Vous pouvez également définir le validateur dans une méthode @InitBinder du contrôleur, avec "binder.setValidator (...)" (auquel cas une combinaison des méthodes 1 et 2 ne serait pas possible, car vous remplacerez la valeur par défaut. validateur). Ou vous pouvez l'instancier dans le constructeur par défaut du contrôleur. Ou bien vous avez un @ Component/@ Service UserValidator que vous injectez (@Autowired) dans votre contrôleur: très utile, car la plupart des validateurs sont singletons + les tests unitaires deviennent plus faciles à simuler + votre validateur peut appeler d'autres composants Spring.

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

Attention: vous ne devez pas confondre traitement de validation avec traitement d'exception. Lisez cet article pour savoir quand les utiliser.

Références :

307
Jerome Dalbert

Il existe deux méthodes pour valider les entrées utilisateur: les annotations et l'héritage de la classe Validator de Spring. Pour les cas simples, les annotations sont Nice. Si vous avez besoin de validations complexes (comme la validation entre champs, par exemple le champ "Vérifier l'adresse e-mail") ou si votre modèle est validé à plusieurs endroits de votre application avec des règles différentes 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 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 classe Foo pourrait ressembler à ceci:

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

Sinon, si vous implémentez le validateur 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 (inutile si vous utilisez des annotations):

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

Voir aussi Spring docs .

J'espère que cela pourra aider.

29
stephen.hanson

Je voudrais prolonger la réponse gentille de Jérôme Dalbert. J'ai trouvé très facile d'écrire vos propres validateurs d'annotation dans le format JSR-303. Vous n'êtes pas limité à la validation "un champ". Vous pouvez créer votre propre annotation au niveau du type et effectuer 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 Jerome De plus, ces validateurs sont "avertis du 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é de 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;

}
12
michal.kreuzman

Si vous avez la même logique de gestion des erreurs pour différents gestionnaires de méthodes, vous obtiendrez 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 souhaitiez renvoyer 400 Bad Request avec des messages d'erreur pour chaque cas d'erreur de validation. Ensuite, la partie de gestion des erreurs sera la même pour chaque point de terminaison REST nécessitant une validation. Répéter la même logique dans chaque gestionnaire n'est pas si SEC ish!

Une façon de résoudre ce problème consiste à supprimer le BindingResult immédiat après chaque bean To-Be-Validated. Maintenant, votre gestionnaire serait comme ça:

@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 sera lancé par Spring. Vous pouvez définir un ControllerAdvice qui gère 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 BindingResult sous-jacent en utilisant la méthode getBindingResult de MethodArgumentNotValidException.

3
Ali Dehghani

Trouver un exemple complet de validation de 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);
    }
}
1
Vicky

Placez ce bean dans votre classe de configuration. 

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

et alors vous pouvez utiliser 

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

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

0
praveen jain