Spring MVC: come eseguire la validazione?

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

Sono confuso circa il modo più pulito e migliore per convalidare l’input dell’utente. Conosco il metodo tradizionale di utilizzo di request.getParameter() e controllo manuale dei nulls , ma non voglio eseguire tutte le convalide nel mio Controller . Alcuni buoni consigli su quest’area saranno molto apprezzati. Non sto utilizzando Hibernate in questa applicazione.

Con Spring MVC, ci sono 3 modi diversi per eseguire la validazione: usando annotazione, manualmente o un mix di entrambi. Non esiste un unico “metodo più pulito e migliore” per la convalida, ma probabilmente esiste uno che si adatta meglio al tuo progetto / problema / contesto.

Diamo un utente:

 public class User { private String name; ... } 

Metodo 1: se si dispone di Spring 3.x + e di una semplice convalida, utilizzare javax.validation.constraints annotazioni javax.validation.constraints (note anche come annotazioni JSR-303).

 public class User { @NotNull private String name; ... } 

Avrai bisogno di un fornitore 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 mapping relazionale, fa solo la convalida :-).

Quindi nel tuo controller avresti qualcosa come:

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

Si noti @Valid: se l’utente ha un nome null, result.hasErrors () sarà true.

Metodo 2: Se hai una convalida complessa (come la logica di convalida di grandi imprese, la convalida condizionale su più campi, ecc.), O per qualche ragione non puoi usare il metodo 1, usa la convalida manuale. È buona norma separare il codice del controller dalla logica di convalida. Non creare le tue classi di validazione da zero, Spring fornisce una comoda interfaccia org.springframework.validation.Validator (dalla spring 2).

Quindi diciamo di avere

 public class User { private String name; private Integer birthYear; private User responsibleUser; ... } 

e si desidera eseguire una convalida “complessa” come: se l’età dell’utente è minore di 18 anni, responsabileUtente non deve essere nullo e responsabile. L’età dell’utente deve essere maggiore di 21 anni.

Farai qualcosa di simile

 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 ci sono errori di validazione, result.hasErrors () sarà vero.

Nota: è anche ansible impostare il validatore in un metodo @InitBinder del controller, con “binder.setValidator (…)” (nel qual caso non sarebbe ansible utilizzare in modo misto il metodo 1 e 2, in quanto si sostituisce l’impostazione predefinita validatore). Oppure puoi istanziarlo nel costruttore predefinito del controller. Oppure hai @ Component / @ Service UserValidator che hai iniettato (@Autowired) nel tuo controller: molto utile, perché la maggior parte dei validatori sono singletons + il test dell’unità di simulazione diventa più facile + il tuo validatore può chiamare altri componenti Spring.

Metodo 3: Perché non utilizzare una combinazione di entrambi i metodi? Convalidare le cose semplici, come l’attributo “nome”, con annotazioni (è veloce da fare, conciso e più leggibile). Mantenere le convalide pesanti per i validatori (quando occorrono ore per codificare annotazioni di convalida complesse personalizzate o solo quando non è ansible utilizzare le annotazioni). L’ho fatto su un progetto precedente, ha funzionato come un fascino, rapido e facile.

Attenzione: non si deve scambiare la gestione della validazione per la gestione delle eccezioni . Leggi questo post per sapere quando usarli.

Riferimenti :

  • Un post sul blog molto interessante sulla convalida del bean (il link originale è morto)
  • Un altro buon post sul blog sulla convalida
  • Ultima documentazione Spring sulla convalida

Esistono due modi per convalidare l’input dell’utente: le annotazioni e ereditando la class 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 email”), o se il tuo modello è convalidato in più punti della tua applicazione con regole diverse, o se non hai la possibilità di modificare il tuo object del modello ponendo annotazioni su di esso, Validator basato sull’eredità di Spring è la strada da percorrere. Mostrerò esempi di entrambi.

La parte di validazione 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 class Foo potrebbe essere simile a:

 public class Foo { @NotNull @Size(min = 1, max = 20) private String name; @NotNull @Min(1) @Max(110) private Integer age; // getters, setters } 

Le annotazioni di cui sopra sono annotazioni javax.validation.constraints . Puoi anche utilizzare Hibernate’s org.hibernate.validator.constraints , ma non sembra che tu stia usando Hibernate.

In alternativa, se si implementa Spring Validator, si creerà una class 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 di cui sopra, è necessario associare anche il validatore al controller Spring (non necessario se si utilizzano annotazioni):

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

Vedi anche i documenti di spring .

Spero possa aiutare.

Vorrei estendere la bella risposta di Jerome Dalbert. Ho trovato molto facile scrivere i propri validatori di annotazioni in modo JSR-303. Non sei limitato ad avere la convalida “un campo”. Puoi creare annotazioni personalizzate a livello di testo e avere una convalida complessa (vedi esempi sotto). Preferisco in questo modo perché non ho bisogno di mescolare diversi tipi di convalida (Spring e JSR-303) come Jerome do. Anche questi validatori sono “Spring aware”, quindi puoi usare @ Inject / @ Autowire out of box.

Esempio di convalida dell’object personalizzato:

 @Target({ TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = { YourCustomObjectValidator.class }) public @interface YourCustomObjectValid { String message() default "{YourCustomObjectValid.message}"; Class[] groups() default {}; Class[] payload() default {}; } public class YourCustomObjectValidator implements ConstraintValidator { @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 dei 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[] 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 { 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; } 

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

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

Supponiamo che tu stia creando servizi RESTful e desideri restituire 400 Bad Request insieme a messaggi di errore per ogni caso di errore di convalida. Quindi, la parte relativa alla gestione degli errori sarebbe la stessa per ogni singolo endpoint REST che richiede la convalida. Ripetere quella stessa logica in ogni singolo handler non è così DRY ish!

Un modo per risolvere questo problema consiste nel rilasciare il BindingResult immediato dopo ogni bean convalidato da to-be . 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 fosse valido, verrà lanciata una MethodArgumentNotValidException da Spring. È ansible definire un ControllerAdvice che 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! } } 

È comunque ansible esaminare il BindingResult sottostante utilizzando il metodo MethodArgumentNotValidException di MethodArgumentNotValidException .

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

Metti questo bean nella tua class di configurazione.

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

e quindi puoi usare

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

per validare un bean manualmente. Quindi otterrai tutti i risultati in BindingResult e potrai recuperarli da lì.