Spring MVC:如何执行validation?

我想知道什么是最干净和最好的方式来执行用户input的表单validation。 我见过一些开发者实现了org.springframework.validation.Validator 。 关于这个问题:我看到它validation了一个类。 该类是否必须用用户input的值手动填充,然后传递给validation器?

我对确认用户input的最简洁和最好的方法感到困惑。 我知道使用request.getParameter() ,然后手动检查nulls的传统方法,但我不想在我的Controller做所有的validation。 有关这方面的一些很好的build议将不胜感激。 我没有在这个应用程序中使用Hibernate。

使用Spring MVC,有三种不同的方法来执行validation:使用注释,手动或两者兼而有之。 没有一种独特的“最干净和最好的方式”来validation,但可能更适合您的项目/问题/背景。

让我们有一个用户:

 public class User { private String name; ... } 

方法1:如果您有Spring 3.x +和简单的validation,请使用javax.validation.constraints注释(也称为JSR-303注释)。

 public class User { @NotNull private String name; ... } 

您需要在库中使用JSR-303提供程序,例如Hibernate Validator ,它是参考实现(这个库与数据库和关系映射无关,只是validation:-)。

然后在你的控制器中,你会有这样的东西:

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

注意@Valid:如果用户碰巧有一个空名称,则result.hasErrors()将为true。

方法2:如果您有复杂的validation(如大型企业validation逻辑,跨多个字段的条件validation等),或由于某种原因您不能使用方法1,请使用手动validation。 将控制器的代码与validation逻辑分开是一个很好的做法。 不要从头创buildvalidation类,Spring提供了一个方便的org.springframework.validation.Validator接口(自Spring 2以来)。

所以我们假设你有

 public class User { private String name; private Integer birthYear; private User responsibleUser; ... } 

你想做一些“复杂的”validation,如:如果用户的年龄在18岁以下,负责用户不能为空,负责用户的年龄必须超过21岁。

你会做这样的事情

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

然后在你的控制器中你会有:

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

如果有validation错误,则result.hasErrors()将为true。

注意:你也可以在控制器的@InitBinder方法中设置validation器,使用“binder.setValidator(…)”(在这种情况下,方法1和方法2的混合使用是不可能的,因为你replace了默认validation器)。 或者你可以在控制器的默认构造函数中实例化它。 或者在你的控制器中注入一个@ Component / @ Service UserValidator(@Autowired):非常有用,因为大多数validation器都是单例+unit testing模拟变得更容易+你的validation器可以调用其他Spring组件。

方法3:为什么不使用这两种方法的组合? validation简单的东西,如“名称”属性,注释(这是快速做,简洁,更具可读性)。 保持validation器的严格validation(当编写自定义的复杂validation注释需要几个小时,或者不可能使用注释时)。 我在一个前项目上做了这个,它像一个魅力,快速和简单的工作。

警告:您不能将validation处理误认为exception处理 。 阅读这篇文章 ,了解何时使用它们。

参考文献

  • 一个非常有趣的博客文章关于beanvalidation ( 原始链接已经死了)
  • 另一个关于validation的好博客文章
  • 有关validation的最新Spring文档

有两种方法来validation用户input:注释和inheritanceSpring的Validator类。 对于简单的情况,注释很好。 如果您需要复杂的validation(如交叉字段validation,例如“validation电子邮件地址”字段),或者您的模型在不同规则的多个位置进行了validation,或者您无法修改通过在其上放置注释来实现模型对象,Spring的基于inheritance的Validator就是要走的路。 我将展示两者的例子。

无论您正在使用哪种types的validation,实际validation部分都是相同的:

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

如果您使用注释,您的Foo类可能如下所示:

 public class Foo { @NotNull @Size(min = 1, max = 20) private String name; @NotNull @Min(1) @Max(110) private Integer age; // getters, setters } 

上面的注释是javax.validation.constraints注释。 你也可以使用Hibernate的org.hibernate.validator.constraints ,但看起来你并不像使用Hibernate。

另外,如果你实现Spring的Validator,你可以创build一个类,如下所示:

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

如果使用上面的validation器,还必须将validation器绑定到Spring控制器(如果使用注释,则不需要):

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

另请参阅Spring文档 。

希望有所帮助。

编辑 – 我决定写一个关于Spring MVCvalidation的教程 。 希望它能帮助一些人。

我想对杰罗姆·达尔伯特(Jerome Dalbert)给出很好的回答。 我发现用JSR-303编写自己的注释validation器非常容易。 您不限于“一场”validation。 您可以在types级别上创build自己的注释并进行复杂的validation(请参阅下面的示例)。 我更喜欢这种方式,因为我不需要像Jerome那样混合不同types的validation(Spring和JSR-303)。 另外这个validation器是“Spring的意识”,所以你可以使用@ Inject / @ Autowire开箱。

自定义对象validation示例:

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

通用字段相等的例子:

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

如果你对不同的方法处理器有相同的error handling逻辑,那么你最终会得到很多具有以下代码模式的处理器:

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

假设您正在创buildRESTful服务,并且想要为每个validation错误情况返回400 Bad Request以及错误消息。 然后,对于需要validation的每个REST端点,error handling部分都是相同的。 在每个处理程序中重复相同的逻辑不是那么简单!

解决这个问题的一个方法是在每个To-Be-Validated bean之后放下立即的BindingResult 。 现在,你的处理程序将是这样的:

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

这样,如果绑定的bean无效, MethodArgumentNotValidException将抛出一个MethodArgumentNotValidExceptionexception。 您可以使用相同的error handling逻辑来定义一个ControllerAdvice来处理这个exception:

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

您仍然可以使用BindingResult getBindingResult方法检查底层的MethodArgumentNotValidException

查找Spring Mvcvalidation的完整示例

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