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.


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 :


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.


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;

}

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


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

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í.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.