Spring MVC:如何执行验证?

问题

我想知道对用户输入执行表单验证的最简洁和最好的方法是什么。我见过一些开发人员实现org.springframework.validation.Validator。关于这一点的问题:我看到它验证了一个类。是否必须使用用户输入中的值手动填充类,然后传递给验证器?

我对用于验证用户输入的最干净和最好的方法感到困惑。我知道使用request.getParameter()的传统方法,然后手动检查nulls,但我不想在myController中进行所有验证。关于这方面的一些好建议将不胜感激。我在这个应用程序中没有使用Hibernate。


#1 热门回答(297 赞)

使用Spring MVC,有3种不同的方法可以执行验证:使用注释,手动或两者兼而有之。没有一种独特的"最干净,最好的方法"来验证,但可能更适合你的项目/问题/背景。

让我们有一个用户:

public class User {

    private String name;

    ...

}

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

public class User {

    @NotNull
    private String name;

    ...

}

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

然后在你的控制器中你会有类似的东西:

@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:**如果你具有复杂的验证(如大型业务验证逻辑,跨多个字段的条件验证等),或者由于某种原因你无法使用方法1,请使用手动验证。将控制器代码与验证逻辑分开是一种很好的做法。不要从头开始创建验证类,Spring提供了一个方便的org.springframework.validation.Validator接口(自Spring 2开始)。

所以,假设你有

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

并且你想做一些"复杂"的验证,如:如果用户的年龄低于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
        }
}

如果存在验证错误,result.hasErrors()将为true。

注意:你还可以使用"binder.setValidator(...)"在控制器的@InitBinder方法中设置验证器(在这种情况下,无法使用方法1和2的混合使用,因为你替换了默认值验证器)。或者你可以在控制器的默认构造函数中实例化它。或者在控制器中注入(@Autowired)@ Component / @ Service UserValidator:非常有用,因为大多数验证器都是单例测试模拟变得更容易验证器可以调用其他Spring组件。

**方法3:**为什么不使用这两种方法的组合?使用注释验证简单的东西,比如"name"属性(它很快,简洁,更易读)。保持验证器的重要验证(如果编写自定义复杂验证注释需要数小时,或者只是在无法使用注释时)。我是在一个以前的项目中做到这一点,它的工作就像一个魅力,快速和简单。

警告:你不得错误验证处理for异常处理.Read this post以了解何时使用它们。

参考文献:

  • 一篇关于bean验证的非常有趣的博客文章(原始链接已死)
  • 关于验证的另一篇好文章
  • 有关验证的最新Spring文档

#2 热门回答(28 赞)

有两种方法可以验证用户输入:注释和继承Spring的Validator类。对于简单的情况,注释很好。如果你需要复杂的验证(例如,跨域验证,例如"验证电子邮件地址"字段),或者你的模型在应用程序的多个位置使用不同的规则进行验证,或者你无法修改模型对象通过在其上放置注释,Spring的基于继承的Validator是要走的路。我将展示两者的例子。

无论你使用哪种类型的验证,实际验证部分都是相同的:

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,你将创建一个类,如下所示:

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

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

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

另见Spring docs

希望有所帮助。


#3 热门回答(12 赞)

我想延伸杰罗姆·达尔伯特的好答案。我发现很容易用JSR-303方式编写自己的注释验证器。你不限于"一个字段"验证。你可以在类型级别创建自己的注释并进行复杂验证(请参阅下面的示例)。我更喜欢这种方式,因为我不需要像Jerome那样混合不同类型的验证(Spring和JSR-303)。此验证器也是"弹簧识别",因此你可以使用@Inject / @ Autowire开箱即用。
自定义对象验证示例:

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

}