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


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

Estoy confundido acerca de la forma más limpia y mejor de validar la entrada del usuario. Conozco el método tradicional de usar request.getParameter() y luego comprobar manualmente para 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.

Author: patstuart, 2012-08-27

6 answers

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

Vamos a tener un Usuario:

public class User {

    private String name;

    ...

}

Método 1: Si tienes Primavera 3.x+ y validación simple para hacer, use javax.validation.constraints anotaciones (también conocidas como anotaciones JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Necesitará un proveedor JSR-303 en su 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 hace validación :-).

Entonces en tu controlador tendrías 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á true.

Método 2: Si tiene validación compleja (como lógica de validación de grandes empresas, validación condicional a través de campos múltiples, etc.), o por alguna razón no puede usar el método 1, use 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 (es) de validación desde cero, Spring proporciona una útil interfaz org.springframework.validation.Validator (desde Spring 2).

Así que digamos que tienes

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

Y desea hacer alguna validación "compleja" como : si la edad del usuario es menor de 18 años, responsibleUser no debe ser null y la edad de responsibleUser debe ser mayor 21.

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 tu controlador tendrías:

@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, el resultado.hasErrors () será true.

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

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

Advertencia : no se debe confundir validación de manejo para manejo de excepciones. Lee este post para saber cuándo usarlos.

Referencias:

 300
Author: Jerome Dalbert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:20

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 validación de campo cruzado, por ejemplo. campo "verificar dirección de correo electrónico"), o si su modelo se valida en varios lugares de su aplicación con reglas diferentes, o si no tiene la capacidad de modificar su objeto de modelo colocando anotaciones en él, el Validador basado en herencia de Spring es el camino a seguir. Voy a 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á usando anotaciones, su clase Foo podría verse como:

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.constraints anotaciones. También puede utilizar Hibernate org.hibernate.validator.constraints, pero no parece que estés usando Hibernación.

Alternativamente, si implementa el Validador de Spring, 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 se utiliza el validador anterior, también tiene que enlazar el validador al controlador de resorte (no es necesario si se usan anotaciones):

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

También ver Spring docs.

Espero que eso ayude.

 29
Author: stephen.hanson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-27 21:33:09

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

Ejemplo de validación de objetos personalizados:

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

}
 12
Author: michal.kreuzman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-26 15:04:34

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

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

Supongamos que está creando servicios RESTful y desea devolver 400 Bad Request junto con mensajes de error para cada caso de error de validación. Luego, la parte de manejo de errores sería la misma para cada extremo REST que requiera validación. Repetir esa misma lógica en cada manejador no es tan SECO ish!

Uno la manera de resolver este problema es dejar caer el inmediato BindingResult después de cada Que Se va a Validar bean. 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 frijol encuadernado no era válido, un MethodArgumentNotValidException será arrojado por la primavera. Puede definir un ControllerAdvice que 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 BindingResult subyacente utilizando el método getBindingResult de MethodArgumentNotValidException.

 3
Author: Ali Dehghani,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-21 14:52:57

Encuentre un ejemplo completo de Validación de 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);
    }
}
 1
Author: Vicky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-15 21:32:00

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. A continuación, obtendrá todos los resultados en BindingResult y se puede recuperar desde allí.

 0
Author: praveen jain,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-15 14:02:36