Spring MVC: Como executar a validação?

156

Gostaria de saber qual é a melhor e mais limpa maneira de executar a validação de formulário das entradas do usuário. Eu já vi alguns desenvolvedores implementarem org.springframework.validation.Validator. Uma pergunta sobre isso: vi que valida uma classe. A classe precisa ser preenchida manualmente com os valores da entrada do usuário e depois passada para o validador?

Estou confuso sobre a maneira mais limpa e melhor de validar a entrada do usuário. Conheço o método tradicional de usar request.getParameter()e, em seguida, verificar manualmente nulls, mas não quero fazer toda a validação no meu Controller. Alguns bons conselhos sobre essa área serão muito apreciados. Não estou usando o Hibernate neste aplicativo.

devdar
fonte

Respostas:

322

Com o Spring MVC, existem três maneiras diferentes de executar a validação: usando anotação, manualmente ou uma mistura de ambas. Não existe uma "maneira mais limpa e melhor" única de 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 o Spring 3.x + e uma validação simples, use as javax.validation.constraintsanotações (também conhecidas como anotações 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 (esta biblioteca não tem nada a ver com bancos de dados e mapeamento relacional, apenas 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ê possui validação complexa (como lógica de validação de grandes empresas, validação condicional em vários campos etc.) ou, por algum motivo, não pode 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 classes de validação do zero, org.springframework.validation.Validatorpois o Spring fornece uma interface prática (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 de 18 anos, o responsávelUsuário não deve ser nulo e a idade do responsável responsável deve ser superior a 21.

Você fará algo assim

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 no método @InitBinder do controlador, com "binder.setValidator (...)" (nesse caso, o uso misto dos métodos 1 e 2 não seria possível, porque você substitui o padrão validador). Ou você pode instancia-lo no construtor padrão do controlador. Ou tenha um @ Component / @ Service UserValidator que você injeta (@Autowired) em seu controlador: muito útil, porque a maioria dos validadores são singletons + a simulação de teste de unidade se torna mais fácil + seu validador pode chamar outros componentes do Spring.

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

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

Referências :

Jerome Dalbert
fonte
você pode me dizer o que meu servlet.xml deve ter para essa configuração? Eu quero passar os erros de volta para o ponto de vista
Devdar
@dev_darin Você quer dizer config para validação JSR-303?
Jerome Dalbert
2
@dev_marin Para validação, no Spring 3.x +, não há nada de especial em "servlet.xml" ou "[nome do servlet] -servlet.xml. Você só precisa do jar do hibernate-validator nas bibliotecas do projeto (ou via Maven). Isso é tudo, deve funcionar então .. Aviso se você usar o Método 3: por padrão, cada controlador terá acesso a um validador JSR-303, portanto, tome cuidado para não substituí-lo por "setValidator". Se desejar adicionar um validador personalizado em top, apenas instanciar-lo e usá-lo, ou injetá-lo (se ele é um componente da Primavera) Se você ainda tiver problemas após a verificação google e doc Spring, você deve postar uma nova pergunta..
Jerome Dalbert
2
Para o uso misto dos métodos 1 e 2, existe uma maneira de usar o @InitBinder. Em vez de "binder.setValidator (...)", pode usar "binder.addValidators (...)"
jasonfungsing
1
Corrija-me se estiver errado, mas você pode misturar a validação via anotações JSR-303 (método 1) e validação personalizada (método 2) ao usar a anotação @InitBinder. Basta usar binder.addValidators (userValidator) em vez de binder.setValidator (userValidator) e os dois métodos de validação entrarão em vigor.
SebastianRiemer
31

Existem duas maneiras de validar a entrada do usuário: anotações e herdar a classe Validator do Spring. Para casos simples, as anotações são boas. Se você precisar de validações complexas (como validação entre campos, por exemplo, campo "verificar endereço de email"), ou se o seu modelo for validado em vários locais do seu aplicativo com regras diferentes, ou se você não tiver a capacidade de modificar seu objeto de modelo colocando anotações nele, o Validator baseado em herança do Spring é o caminho a percorrer. Vou mostrar exemplos de ambos.

A parte da 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 anotações, sua Fooclasse poderá se parecer com:

public class Foo {

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

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

    // getters, setters
}

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

Como alternativa, se você implementar o Validador do Spring, criará uma classe 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 precisará vincular o validador ao controlador Spring (não necessário se estiver usando anotações):

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

Veja também os documentos do Spring .

Espero que ajude.

stephen.hanson
fonte
ao usar o Validator da Spring, tenho que definir o pojo do controlador e validá-lo?
Devdar
Não sei se entendi sua pergunta. Se você vir o snippet de código do controlador, o Spring vinculará automaticamente o formulário enviado ao Fooparâmetro no método manipulador. Você pode esclarecer?
27612 stephen.hanson
ok, o que estou dizendo é que, quando o usuário envia as entradas do usuário, o Controller obtém a solicitação http, a partir daí o que acontece é que você usa o request.getParameter () para obter todos os parâmetros do usuário, em seguida, definir os valores no POJO e passar a classe para o objeto de validação. A classe de validação enviará os erros de volta à exibição com os erros, se houver algum. É assim que acontece?
Devdar
1
Isso aconteceria assim, mas há uma maneira mais simples ... Se você usar JSP e um envio <form: form commandName = "user">, os dados serão automaticamente colocados no usuário @ModelAttribute ("user") no controlador método. Consulte o documento: static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert
+1 porque esse foi o primeiro exemplo que encontrei que usa @ModelAttribute; sem ele, nenhum dos tutoriais que encontrei funciona.
Riccardo Cossu
12

Gostaria de estender uma 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 de "um campo". Você pode criar sua própria anotação no nível de tipo e ter validação complexa (veja exemplos abaixo). Prefiro assim, porque não preciso misturar diferentes tipos de validação (Spring e JSR-303) como Jerome. Além disso, esses validadores são "cientes da mola", para que você possa usar @ Inject / @ Autowire fora da caixa.

Exemplo de validação de objeto customizado:

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

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<? 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
fonte
1
Eu também estava imaginando que um controlador geralmente tem um validador e vi onde você pode ter vários validadores, no entanto, se você tiver um conjunto de validação definido para um objeto, no entanto, a operação que você deseja executar no objeto é diferente, por exemplo, salvar / atualizar um salvar um determinado conjunto de validação é necessário e uma atualização um conjunto diferente de validação é necessária. Existe uma maneira de configurar a classe validadora para armazenar toda a validação com base na operação ou você precisará usar vários validadores?
Devdar
1
Você também pode ter uma validação de anotação no método Portanto, você pode criar sua própria "validação de domínio" se entender sua pergunta. Para isso, você deve especificar ElementType.METHODno @Target.
Michal.kreuzman
Eu entendo o que você está dizendo. Você também pode me indicar um exemplo para uma imagem mais clara.
Devdar
4

Se você tiver a mesma lógica de manipulação de erros para diferentes manipuladores de métodos, você acabará com 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 deseje retornar 400 Bad Requestjunto com mensagens de erro para cada caso de erro de validação. Em seguida, a parte de manipulação de erros seria a mesma para todos os terminais REST que requerem validação. Repetir essa mesma lógica em cada manipulador não é tão DRY ish!

Uma maneira de resolver esse problema é eliminar o imediato BindingResultapós cada bean A validar . 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, a MethodArgumentNotValidExceptionserá lançada pelo Spring. Você pode definir um ControllerAdviceque lide 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 método subjacente BindingResultusando .getBindingResultMethodArgumentNotValidException

Ali Dehghani
fonte
1

Encontre um exemplo completo da validação do 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
fonte
0

Coloque esse bean em sua classe de configuração.

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

e então você pode usar

 <T> 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 poderá recuperar a partir daí.

praveen jain
fonte