Spring MVC: come eseguire la convalida?


156

Vorrei sapere qual è il modo più pulito e migliore per eseguire la convalida del modulo degli input dell'utente. Ho visto implementare alcuni sviluppatori org.springframework.validation.Validator. Una domanda al riguardo: ho visto che convalida una classe. La classe deve essere compilata manualmente con i valori dall'input dell'utente e quindi passata al validatore?

Sono confuso sul modo più pulito e migliore per convalidare l'input dell'utente. Conosco il metodo tradizionale di utilizzo request.getParameter()e controllo manuale nulls, ma non voglio fare tutta la convalida nel mio Controller. Alcuni buoni consigli su quest'area saranno molto apprezzati. Non sto usando Hibernate in questa applicazione.


Risposte:


322

Con Spring MVC, ci sono 3 modi diversi per eseguire la convalida: usare l'annotazione, manualmente o una combinazione di entrambi. Non esiste un unico "modo più pulito e migliore" per convalidare, ma ce n'è probabilmente uno che si adatta meglio al tuo progetto / problema / contesto.

Diamo un utente:

public class User {

    private String name;

    ...

}

Metodo 1: se hai Spring 3.x + e una semplice validazione da fare, usa le javax.validation.constraintsannotazioni (note anche come annotazioni JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Avrai bisogno di un provider JSR-303 nelle tue librerie, come Hibernate Validator che è l'implementazione di riferimento (questa libreria non ha nulla a che fare con i database e la mappatura relazionale, fa solo la validazione :-).

Quindi nel tuo controller avresti qualcosa del tipo:

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

Nota @Valid: se l'utente ha un nome null, result.hasErrors () sarà true.

Metodo 2: se si dispone di una convalida complessa (come la logica di convalida delle grandi aziende, la convalida condizionale su più campi, ecc.) O per qualche motivo non è possibile utilizzare il metodo 1, utilizzare la convalida manuale. È buona norma separare il codice del controller dalla logica di convalida. Non creare da zero le tue classi di convalida, Spring offre un'interfaccia pratica org.springframework.validation.Validator(dalla primavera 2).

Quindi diciamo che hai

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

e si desidera effettuare una convalida "complessa" come: se l'età dell'utente è inferiore a 18 anni, l'Utente responsabile non deve essere nullo e l'età dell'Utente responsabile deve essere superiore a 21 anni.

Farai qualcosa del genere

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

    }

}

Quindi nel tuo controller avresti:

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

Se sono presenti errori di convalida, result.hasErrors () sarà true.

Nota: è anche possibile impostare il validatore in un metodo @InitBinder del controller, con "binder.setValidator (...)" (nel qual caso un uso misto dei metodi 1 e 2 non sarebbe possibile, poiché si sostituisce il valore predefinito validatore). Oppure potresti istanziarlo nel costruttore predefinito del controller. O avere un UserValidator @ Component / @ Service che si inietta (@Autowired) nel controller: molto utile, perché la maggior parte dei validatori sono singletons + il test dell'unità di derisione diventa più facile + il validatore potrebbe chiamare altri componenti Spring.

Metodo 3: perché non utilizzare una combinazione di entrambi i metodi? Convalida le cose semplici, come l'attributo "nome", con le annotazioni (è veloce, conciso e più leggibile). Mantieni le convalide pesanti per i validatori (quando occorrerebbero ore per codificare annotazioni di convalida complesse personalizzate o solo quando non è possibile utilizzare le annotazioni). L'ho fatto su un precedente progetto, ha funzionato come un fascino, veloce e facile.

Avviso: non si deve confondere la gestione della convalida con la gestione delle eccezioni . Leggi questo post per sapere quando usarli.

Riferimenti :


puoi dirmi cosa dovrebbe avere il mio servlet.xml per questa configurazione. Voglio riportare gli errori alla vista
devdar,

@dev_darin Intendi la configurazione per la convalida JSR-303?
Jerome Dalbert,

2
@dev_marin Per la convalida, nella primavera 3.x +, non c'è nulla di speciale in "servlet.xml" o "[nome servlet] -servlet.xml. Hai solo bisogno di un vaso di convalida ibernazione nelle librerie del progetto (o tramite Maven). Questo è tutto, dovrebbe funzionare quindi Avvertenza se si utilizza il Metodo 3: per impostazione predefinita, ciascun controller ha accesso a un validatore JSR-303, quindi fare attenzione a non sovrascriverlo con "setValidator". Se si desidera aggiungere un validatore personalizzato su in alto, basta istanziarlo e usarlo o iniettarlo (se si tratta di un componente Spring). Se i problemi persistono anche dopo aver controllato Google e il documento Spring, è necessario pubblicare una nuova domanda:
Jerome Dalbert

2
Per l'uso misto dei metodi 1 e 2, esiste un modo per usare @InitBinder. Invece di "binder.setValidator (...)", puoi usare "binder.addValidators (...)"
jasonfungsing

1
Correggimi se sbaglio, ma puoi mescolare la convalida tramite le annotazioni JSR-303 (Metodo 1) e la convalida personalizzata (Metodo 2) quando usi l'annotazione @InitBinder. Utilizzare semplicemente binder.addValidators (userValidator) anziché binder.setValidator (userValidator) ed entrambi i metodi di convalida avranno effetto.
SebastianRiemer,

31

Esistono due modi per convalidare l'input dell'utente: le annotazioni e ereditando la classe Validator di Spring. Per casi semplici, le annotazioni sono belle. Se hai bisogno di convalide complesse (come la convalida cross-field, ad es. Campo "verifica indirizzo e-mail") o se il tuo modello è convalidato in più punti dell'applicazione con regole diverse o se non hai la possibilità di modificare il tuo modello oggetto posizionando le annotazioni su di esso, il validatore basato sull'ereditarietà di Spring è la strada da percorrere. Mostrerò esempi di entrambi.

La parte di convalida effettiva è la stessa indipendentemente dal tipo di convalida che stai utilizzando:

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

Se stai usando le annotazioni, la tua Fooclasse potrebbe apparire come:

public class Foo {

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

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

    // getters, setters
}

Le annotazioni sopra sono javax.validation.constraintsannotazioni. Puoi anche usare Hibernate org.hibernate.validator.constraints, ma non sembra che tu stia usando Hibernate.

In alternativa, se si implementa Spring's Validator, è necessario creare una classe come segue:

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]");
        }
    }
}

Se si utilizza il validatore sopra, è necessario anche associare il validatore al controller Spring (non necessario se si utilizzano le annotazioni):

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

Vedi anche i documenti di primavera .

Spero che aiuti.


quando uso Spring's Validator devo impostare il pojo dal controller e quindi convalidarlo?
devdar,

Non sono sicuro di aver capito la tua domanda. Se vedi lo snippet di codice del controller, Spring associa automaticamente il modulo inviato al Fooparametro nel metodo del gestore. Puoi chiarire?
stephen.hanson,

ok quello che sto dicendo è quando l'utente invia gli input dell'utente il Controller riceve la richiesta http, da lì cosa succede è che usi request.getParameter () per ottenere tutti i parametri utente quindi impostare i valori nel POJO e poi passare la classe per l'oggetto di validazione. La classe di convalida restituirà gli errori alla vista con gli eventuali errori trovati. È così che succede?
devdar,

1
Succederebbe in questo modo ma esiste un modo più semplice ... Se si utilizza JSP e un <form: form commandName = "user">, i dati vengono automaticamente inseriti nell'utente @ModelAttribute ("user") nel controller metodo. Vedi il documento: static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert

+1 perché è il primo esempio che ho trovato che utilizza @ModelAttribute; senza di essa nessuno dei tutorial che ho trovato funziona.
Riccardo Cossu,

12

Vorrei estendere la bella risposta di Jerome Dalbert. Ho trovato molto facile scrivere i tuoi validatori di annotazioni in modo JSR-303. Non sei limitato ad avere la convalida "un campo". Puoi creare la tua annotazione a livello di tipo e avere una validazione complessa (vedi esempi sotto). Preferisco così perché non ho bisogno di mescolare diversi tipi di validazione (Spring e JSR-303) come fanno Jerome. Anche questi validatori sono "compatibili con Spring", quindi puoi usare @ Inject / @ Autowire out of box.

Esempio di validazione di oggetti personalizzati:

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

Esempio di uguaglianza di campi generici:

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
Mi chiedevo anche che un controller di solito abbia un validatore e ho visto dove puoi avere più validatori, tuttavia se hai un set di validazione definito per un oggetto, tuttavia l'operazione che desideri preformare sull'oggetto è diversa, ad esempio un salvataggio / aggiornamento per un è necessario salvare un determinato set di validazione ed è richiesto un aggiornamento un diverso set di validazione. Esiste un modo per configurare la classe validator per contenere tutta la convalida in base all'operazione o sarà necessario utilizzare più validatori?
devdar,

1
Puoi anche avere una convalida delle annotazioni sul metodo. Quindi puoi creare la tua "convalida del dominio" se capisco la tua domanda. Per questo è necessario specificare ElementType.METHODin @Target.
michal.kreuzman,

capisco cosa stai dicendo puoi anche indicarmi un esempio per un'immagine più chiara.
devdar,

4

Se hai la stessa logica di gestione degli errori per gestori di metodi diversi, finiresti con molti gestori con il seguente modello di codice:

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

Supponiamo di creare servizi RESTful e di voler tornare 400 Bad Requestinsieme a messaggi di errore per ogni caso di errore di convalida. Quindi, la parte di gestione degli errori sarebbe la stessa per ogni singolo endpoint REST che richiede la convalida. Ripetere la stessa logica in ogni singolo gestore non è così secco !

Un modo per risolvere questo problema è eliminare l'immediato BindingResultdopo ogni bean da convalidare . Ora, il tuo gestore sarebbe così:

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

In questo modo, se il bean associato non era valido, a MethodArgumentNotValidExceptionverrà lanciato a da Spring. È possibile definire un oggetto ControllerAdviceche gestisce questa eccezione con la stessa logica di gestione degli errori:

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

Puoi ancora esaminare il sottostante BindingResultusando il getBindingResultmetodo di MethodArgumentNotValidException.


1

Trova un esempio completo di convalida del 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);
    }
}

0

Inserisci questo bean nella tua classe di configurazione.

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

e quindi puoi usare

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

per la convalida manuale di un bean. Quindi otterrai tutti i risultati in BindingResult e puoi recuperare da lì.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.