java - requestmapping - spring validation example
Spring MVC: ¿cómo realizar la validación? (6)
Con Spring MVC, hay 3 formas diferentes de realizar la validación: utilizando anotaciones, manualmente o una combinación de ambas. No hay una única "forma más limpia y mejor" de validar, pero probablemente haya una que se ajuste 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.constraints
anotaciones javax.validation.constraints
(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 las bases de datos y el 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 la lógica de validación de una gran empresa, validación condicional en múltiples campos, etc.), o por algún motivo 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 sus clases de validación desde cero, Spring proporciona una práctica interfaz org.springframework.validation.Validator
(desde Spring 2).
Entonces digamos que tienes
public class User {
private String name;
private Integer birthYear;
private User responsibleUser;
...
}
y desea realizar una validación "compleja" como: si la edad del usuario es menor de 18 años, el usuario responsable no debe ser nulo y responsable. La edad del usuario 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
}
}
Luego, 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 usar el método 1 y 2 porque no puede cambiar el valor predeterminado) validador). O puede crear una instancia en el constructor predeterminado del controlador. O tenga un @Component / @ Service UserValidator que inyecte (@Autowired) en su controlador: muy útil, ya que la mayoría de los validadores son singletons + la burla de prueba unitaria se vuelve más fácil + su validador podría llamar a otros componentes de 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 de hacer, conciso y más legible). Mantenga las validaciones pesadas para validadores (cuando tomaría horas codificar anotaciones de validación complejas personalizadas, o simplemente cuando no sea posible usar anotaciones). Hice esto en un proyecto anterior, funcionó como un encanto, rápido y fácil.
Advertencia: no debe confundir el manejo de la validación para el manejo de excepciones . Lee esta publicación para saber cuándo usarla.
Referencias
Me gustaría saber cuál es la forma más limpia y mejor para 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: lo vi validar una clase. ¿La clase debe 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. Conozco el método tradicional de usar request.getParameter()
y luego verifico manualmente los nulls
, pero no quiero hacer toda la validación en mi Controller
. Algunos buenos consejos en esta área serán muy apreciados. No estoy usando Hibernate en esta aplicación.
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);
}
}
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 objeto colocando anotaciones en él, validador basado en la herencia de primavera 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 clase Foo
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 anotaciones de javax.validation.constraints
. También puede usar las org.hibernate.validator.constraints
de org.hibernate.validator.constraints
de Hibernate, 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());
}
También vea los documentos de Spring .
Espero que ayude.
Me gustaría extender una buena respuesta de Jerome Dalbert. Encontré muy fácil escribir sus propios validadores de anotación en modo JSR-303. No está limitado a la validación de "un campo". Puede crear su propia anotación en el 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 hace Jerome. Además, estos validadores son "conscientes de la primavera", por lo que puede usar @ Inject / @ Autowire fuera de la caja.
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;
}
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 podrá recuperarlos desde allí.
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
}
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. 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 manejador individual no es tan DRY ish!
Una forma de resolver este problema es soltar el BindingResult
inmediato después de cada BindingResult
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 forma, si el bean encuadernado no fuera válido, MethodArgumentNotValidException
lanzará una MethodArgumentNotValidException
. 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!
}
}
Aún puede examinar el BindingResult
subyacente utilizando el método MethodArgumentNotValidException
de MethodArgumentNotValidException
.