Spring MVC: ¿Cómo realizar la validación?

156

Me gustaría saber cuál es la forma más limpia y mejor de realizar la validación de formularios de las entradas de los usuarios. He visto algunos desarrolladores implementar org.springframework.validation.Validator. Una pregunta sobre eso: vi que valida una clase. ¿La clase tiene que llenarse manualmente con los valores de la entrada del usuario y luego pasar al validador?

Estoy confundido acerca de la forma más limpia y mejor para validar la entrada del usuario. Sé sobre el método tradicional de usar request.getParameter()y luego verificar manualmente nulls, pero no quiero hacer toda la validación en mi Controller. Algunos buenos consejos sobre esta área serán muy apreciados. No estoy usando Hibernate en esta aplicación.

devdar
fuente

Respuestas:

322

Con Spring MVC, hay 3 formas diferentes de realizar la validación: usando anotaciones, manualmente o una combinación de ambas. No existe una "forma más limpia y mejor" única para validar, pero probablemente haya una que se adapte mejor a su proyecto / problema / contexto.

Tengamos un usuario:

public class User {

    private String name;

    ...

}

Método 1: si tiene Spring 3.x + y una validación simple que hacer, use javax.validation.constraintsanotaciones (también conocidas como anotaciones JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Necesitará un proveedor JSR-303 en sus bibliotecas, como Hibernate Validator que es la implementación de referencia (esta biblioteca no tiene nada que ver con bases de datos y mapeo relacional, solo valida :-).

Luego, en su controlador, tendría 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 el @Valid: si el usuario tiene un nombre nulo, result.hasErrors () será verdadero.

Método 2: Si tiene una validación compleja (como lógica de validación de grandes empresas, validación condicional en múltiples campos, etc.), o por alguna razón no puede usar el método 1, use la validación manual. Es una buena práctica separar el código del controlador de la lógica de validación. No cree su (s) clase (s) de validación desde cero, Spring proporciona una org.springframework.validation.Validatorinterfaz práctica (desde Spring 2).

Entonces digamos que tienes

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

y desea hacer una validación "compleja" como: si la edad del usuario es menor de 18 años, el usuario responsable no debe ser nulo y la edad del usuario responsable debe ser mayor de 21 años.

Harás algo como esto

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

    }

}

Entonces en su controlador tendría:

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

Si hay errores de validación, result.hasErrors () será verdadero.

Nota: También puede configurar el validador en un método @InitBinder del controlador, con "binder.setValidator (...)" (en cuyo caso no sería posible un uso mixto de los métodos 1 y 2, ya que reemplaza el valor predeterminado validador). O podría crear una instancia en el constructor predeterminado del controlador. O tenga un @ Component / @ Service UserValidator que inyecte (@Autowired) en su controlador: muy útil, porque la mayoría de los validadores son singletons + la prueba de unidad se burla más fácilmente + su validador podría llamar a otros componentes Spring.

Método 3: ¿Por qué no usar una combinación de ambos métodos? Valide las cosas simples, como el atributo "nombre", con anotaciones (es rápido, conciso y más legible). Mantenga las validaciones pesadas para los validadores (cuando tomaría horas codificar anotaciones de validación complejas personalizadas, o simplemente cuando no es posible usar anotaciones). Hice esto en un proyecto anterior, funcionó de maravilla, rápido y fácil.

Advertencia: no debe confundir el manejo de validación con el manejo de excepciones . Lea esta publicación para saber cuándo usarlos.

Referencias

Jerome Dalbert
fuente
¿puede decirme qué debería tener mi servlet.xml para esta configuración? Quiero devolver los errores a la vista
devdar
@dev_darin ¿Te refieres a la configuración para la validación JSR-303?
Jerome Dalbert
2
@dev_marin Para la validación, en Spring 3.x +, no hay nada especial en "servlet.xml" o "[nombre-servlet] -servlet.xml. Solo necesita un jar de validación de hibernación en las bibliotecas de su proyecto (o mediante Maven). Eso es todo, entonces debería funcionar. Advertencia si utiliza el Método 3: por defecto, cada controlador tiene acceso a un validador JSR-303, así que tenga cuidado de no anularlo con "setValidator". Si desea agregar un validador personalizado en arriba, solo ejecútelo y úselo, o inyéctelo (si es un componente Spring). Si aún tiene problemas después de consultar google y Spring doc, debe publicar una nueva pregunta.
Jerome Dalbert
2
Para el uso mixto de los métodos 1 y 2, hay una manera de usar @InitBinder. En lugar de "binder.setValidator (...)", puede usar "binder.addValidators (...)"
jasonfungsing
1
Corríjame si me equivoco, pero puede mezclar la validación a través de anotaciones JSR-303 (Método 1) y la validación personalizada (Método 2) cuando use la anotación @InitBinder. Simplemente use binder.addValidators (userValidator) en lugar de binder.setValidator (userValidator) y ambos métodos de Validación surtirán efecto.
SebastianRiemer
31

Hay dos formas de validar la entrada del usuario: anotaciones y heredando la clase Validator de Spring. Para casos simples, las anotaciones son agradables. Si necesita validaciones complejas (como la validación de campo cruzado, por ejemplo, el campo "verificar dirección de correo electrónico"), o si su modelo está validado en varios lugares de su aplicación con reglas diferentes, o si no tiene la capacidad de modificar su modelo de objeto colocando anotaciones en él, el Validador basado en herencia de Spring es el camino a seguir. Mostraré ejemplos de ambos.

La parte de validación real es la misma independientemente del tipo de validación que esté utilizando:

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

Si está utilizando anotaciones, su Fooclase podría verse así:

public class Foo {

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

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

    // getters, setters
}

Las anotaciones anteriores son javax.validation.constraintsanotaciones. También puedes usar Hibernate's org.hibernate.validator.constraints , pero no parece que esté usando Hibernate.

Alternativamente, si implementa Spring's Validator, crearía una clase de la siguiente manera:

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

Si usa el validador anterior, también debe vincular el validador al controlador Spring (no es necesario si usa anotaciones):

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

Ver también Spring docs .

Espero que ayude.

stephen.hanson
fuente
cuando uso Spring's Validator, ¿tengo que configurar el pojo desde el controlador y luego validarlo?
devdar
No estoy seguro de entender tu pregunta. Si ve el fragmento de código del controlador, Spring vincula automáticamente el formulario enviado al Fooparámetro en el método del controlador. ¿Puedes aclarar?
stephen.hanson
ok, lo que digo es que cuando el usuario envía las entradas del usuario, el controlador obtiene la solicitud http, a partir de ahí, lo que sucede es que usa el request.getParameter () para obtener todos los parámetros del usuario, luego establece los valores en el POJO y luego pasa la clase al objeto de validación. La clase de validación enviará los errores de vuelta a la vista con los errores si se encuentran. ¿Es así como sucede?
devdar
1
Sucedería así, pero hay una manera más simple ... Si usa JSP y un envío <form: form commandName = "user">, los datos se colocan automáticamente en el usuario @ModelAttribute ("user") en el controlador método. Ver el documento: static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert
+1 porque ese es el primer ejemplo que encontré que usa @ModelAttribute; sin él, ninguno de los tutoriales que encontré funciona.
Riccardo Cossu
12

Me gustaría extender la buena respuesta de Jerome Dalbert. Encontré muy fácil escribir sus propios validadores de anotación en forma JSR-303. No está limitado a tener la validación de "un campo". Puede crear su propia anotación a nivel de tipo y tener una validación compleja (consulte los ejemplos a continuación). Prefiero esta forma porque no necesito mezclar diferentes tipos de validación (Spring y JSR-303) como lo hace Jerome. Además, estos validadores son "conscientes de Spring", por lo que puede utilizar @ Inject / @ Autowire fuera de la caja.

Ejemplo de validación de objeto personalizado:

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

Ejemplo de igualdad 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
fuente
1
También me preguntaba si un controlador generalmente tiene un validador y vi dónde puede tener múltiples validadores, sin embargo, si tiene un conjunto de validación definido para un objeto, la operación que desea realizar en el objeto es diferente, por ejemplo, guardar / actualizar para un se requiere guardar un cierto conjunto de validación y una actualización se requiere un conjunto diferente de validación. ¿Hay alguna forma de configurar la clase de validador para que contenga toda la validación en función de la operación o necesitará usar validadores múltiples?
devdar
1
También puede tener una validación de anotación en el método. Así que puedes crear tu propia "validación de dominio" si entiendo tu pregunta. Para esto debes especificar ElementType.METHODen @Target.
michal.kreuzman
Entiendo lo que estás diciendo, ¿también puedes señalarme un ejemplo para una imagen más clara?
devdar
4

Si tiene la misma lógica de manejo de errores para diferentes manejadores de métodos, entonces terminaría con muchos manejadores con el siguiente patrón de código:

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

Suponga que está creando servicios RESTful y desea regresar 400 Bad Requestjunto con mensajes de error para cada caso de error de validación. Entonces, la parte de manejo de errores sería la misma para cada punto final REST que requiera validación. ¡Repetir esa misma lógica en cada controlador no es tan SECO !

Una forma de resolver este problema es eliminar el elemento inmediatamente BindingResultdespués de cada bean To-Be-Validated . Ahora, su controlador sería así:

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

De esta manera, si el bean enlazado no era válido, MethodArgumentNotValidExceptionSpring lanzará un a. Puede definir un ControllerAdviceque maneje esta excepción con la misma lógica de manejo de errores:

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

Todavía puede examinar el subyacente BindingResultutilizando el getBindingResultmétodo de MethodArgumentNotValidException.

Ali Dehghani
fuente
1

Encuentre un ejemplo completo de validación 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
fuente
0

Pon este bean en tu clase de configuración.

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

y luego puedes usar

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

para validar un bean manualmente. Entonces obtendrá todos los resultados en BindingResult y podrá recuperarlos desde allí.

Praveen Jain
fuente