Spring MVC: Como realizar a validação?

Eu gostaria de saber qual é a maneira mais limpa e melhor para executar a validação de formulário das inputs do usuário. Eu vi alguns desenvolvedores implementarem org.springframework.validation.Validator . Uma pergunta sobre isso: eu vi valida uma class. A class deve ser preenchida manualmente com os valores da input do usuário e depois passada para o validador?

Estou confuso sobre a maneira mais limpa e melhor para validar a input do usuário. Eu sei sobre o método tradicional de usar request.getParameter() e, em seguida, verificar manualmente para nulls , mas eu não quero fazer toda a validação no meu Controller . Alguns bons conselhos sobre esta área serão muito apreciados. Eu não estou usando o Hibernate nesta aplicação.

Com o Spring MVC, existem 3 maneiras diferentes de executar a validação: usando anotação, manualmente ou uma combinação de ambas. Não existe uma única “forma mais limpa e melhor” para validar, mas provavelmente existe uma que se adapte melhor ao seu projeto / problema / contexto.

Vamos ter um usuário:

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

Método 1: Se você tiver Spring 3.x + e validação simples para fazer, use javax.validation.constraints annotations javax.validation.constraints (também conhecidas como annotations JSR-303).

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

Você precisará de um provedor JSR-303 em suas bibliotecas, como o Hibernate Validator, que é a implementação de referência (essa biblioteca não tem nada a ver com bancos de dados e mapeamento relacional, apenas faz validação :-).

Então, no seu controlador, você teria algo como:

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

Observe o @Valid: se o usuário tiver um nome nulo, result.hasErrors () será verdadeiro.

Método 2: Se você tiver uma validação complexa (como lógica de validação de grandes empresas, validação condicional em vários campos, etc.), ou por algum motivo você não puder usar o método 1, use a validação manual. É uma boa prática separar o código do controlador da lógica de validação. Não crie suas classs de validação do zero, o Spring fornece uma interface útil org.springframework.validation.Validator (desde o Spring 2).

Então digamos que você tenha

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

e você deseja fazer uma validação “complexa” como: se a idade do usuário for menor que 18 anos, o responsável não deverá ser nulo e a idade do usuário responsável deverá ser maior que 21 anos.

Você vai fazer algo parecido com isto

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

Então, no seu controlador, você teria:

 @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 houver erros de validação, result.hasErrors () será verdadeiro.

Nota: Você também pode definir o validador em um método @InitBinder do controlador, com “binder.setValidator (…)” (nesse caso, um uso de mix dos methods 1 e 2 não seria possível, porque você substitui o padrão validador). Ou você poderia instanciá-lo no construtor padrão do controlador. Ou tenha um @ Component User Service / @ UserValidator que você injeta (@Autowired) no seu controlador: muito útil, porque a maioria dos validadores são singletons + o teste de unidade de zombaria torna-se mais fácil + seu validador pode chamar outros componentes Spring.

Método 3: Por que não usar uma combinação dos dois methods? Valide as coisas simples, como o atributo “name”, com annotations (é rápido, conciso e mais legível). Mantenha as validações pesadas para os validadores (quando levaria horas para codificar annotations de validação complexas personalizadas ou apenas quando não for possível usar annotations). Eu fiz isso em um projeto anterior, funcionou como um encanto, rápido e fácil.

Aviso: você não deve confundir o tratamento de validação para o tratamento de exceções . Leia este post para saber quando usá-los.

Referências :

  • Um post muito interessante sobre validação de beans (o link original está morto)
  • Outra boa postagem no blog sobre validação
  • Última documentação da Spring sobre validação

Existem duas maneiras de validar a input do usuário: annotations e herdando a class Validator do Spring. Para casos simples, as annotations são boas. Se você precisar de validações complexas (como validação entre campos, por exemplo, campo “verificar endereço de e-mail”), ou se seu modelo for validado em vários locais em seu aplicativo com regras diferentes ou se você não tiver a capacidade de modificar object de modelo, colocando annotations sobre ele, Validador baseado em inheritance da spring é o caminho a percorrer. Eu vou mostrar exemplos de ambos.

A parte de validação real é a mesma, independentemente do tipo de validação que você está usando:

 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 você estiver usando annotations, sua class Foo pode parecer com:

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

Anotações acima são annotations javax.validation.constraints . Você também pode usar o org.hibernate.validator.constraints do Hibernate, mas não parece que você está usando o Hibernate.

Como alternativa, se você implementar o Spring’s Validator, você criaria uma class da seguinte maneira:

 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 estiver usando o validador acima, você também terá que ligar o validador ao controlador Spring (não necessário se estiver usando annotations):

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

Veja também docs da primavera .

Espero que ajude.

Eu gostaria de estender boa resposta de Jerome Dalbert. Achei muito fácil escrever seus próprios validadores de anotação no modo JSR-303. Você não está limitado a ter a validação “um campo”. Você pode criar sua própria anotação no nível do tipo e ter uma validação complexa (veja os exemplos abaixo). Eu prefiro assim porque não preciso misturar diferentes tipos de validação (Spring e JSR-303) como Jerome do. Além disso, esses validadores são “compatíveis com o Spring”, portanto você pode usar o @Inject / @ Autowire fora da checkbox.

Exemplo de validação de object personalizado:

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

Exemplo de igualdade de campos genéricos:

 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 você tiver a mesma lógica de tratamento de erros para diferentes manipuladores de método, acabará tendo muitos manipuladores com o seguinte padrão de código:

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

Suponha que você esteja criando serviços RESTful e queira retornar 400 Bad Request junto com mensagens de erro para cada caso de erro de validação. Em seguida, a parte de tratamento de erros seria a mesma para todos os terminais REST que exigem validação. Repetir essa mesma lógica em cada manipulador não é tão seco !

Uma maneira de resolver esse problema é eliminar o BindingResult imediato após cada bean To-Be-Validated . Agora, seu manipulador seria assim:

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

Dessa forma, se o bean vinculado não for válido, um MethodArgumentNotValidException será lançado pelo Spring. Você pode definir um ControllerAdvice que lida com essa exceção com a mesma lógica de tratamento de erros:

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

Você ainda pode examinar o BindingResult subjacente usando o método MethodArgumentNotValidException de MethodArgumentNotValidException .

Encontre o exemplo completo 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); } } 

Coloque esse bean na sua class de configuração.

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

e então você pode usar

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

para validar um bean manualmente. Então você obterá todos os resultados em BindingResult e você poderá recuperar a partir daí.