Spring MVC:如何执行验证?


156

我想知道什么是执行用户输入的表单验证的最干净,最好的方法。我已经看到一些开发人员实现了org.springframework.validation.Validator。有一个问题:我看到它验证了一个类。是否必须使用用户输入的值手动填充该类,然后将其传递给验证器?

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


Answers:


322

使用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岁,则负责用户不能为null,负责用户的年龄必须超过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,因为您替换了默认值验证器)。或者,您可以在控制器的默认构造函数中实例化它。或者在控制器中插入一个@ Component / @ Service UserValidator:非常有用,因为大多数验证器都是单例+单元测试模拟变得更容易+验证器可以调用其他Spring组件。

方法3: 为什么不同时使用两种方法?验证带有注解的简单内容,例如“ name”属性(该操作快速,简洁且可读性强)。保留对验证者的繁重验证(当编写自定义复杂验证注释需要花费数小时时,或者仅在无法使用注释时)。我在一个以前的项目中做到了这一点,它就像一个魅力,快速又轻松。

警告:你不能错误验证处理异常处理阅读这篇文章,了解何时使用它们。

参考文献:


您能告诉我该配置中的servlet.xml应该具有什么内容。我想将错误传递回视图
devdar 2012年

@dev_darin您的意思是用于JSR-303验证的配置?
Jerome Dalbert

2
@dev_marin对于验证,在Spring 3.x +中,“ servlet.xml”或“ [servlet-name] -servlet.xml”没有什么特别的。您只需要在项目库中(或通过Maven)使用hibernate-validator jar。仅此而已,然后便可以正常工作。警告:如果使用方法3:默认情况下,每个控制器都可以访问JSR-303验证器,因此请注意不要用“ setValidator”覆盖它。顶部,只是实例并使用它,或注入它(如果它是一个Spring组件)如果您还有检查谷歌和Spring文档后的问题,您应该发布一个新的问题。
杰罗姆Dalbert

2
对于方法1和2的混合使用,有一种使用@InitBinder的方法。代替“ binder.setValidator(...)”,可以使用“ binder.addValidators(...)”
jasonfungsing 2014年

1
如果我错了,请指正,但使用@InitBinder注释时,可以通过JSR-303批注(方法1)和自定义验证(方法2)混合使用验证。只需使用binder.addValidators(userValidator)代替binder.setValidator(userValidator),这两种验证方法都将生效。
SebastianRiemer

31

有两种验证用户输入的方法:批注和通过继承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

希望有帮助。


使用Spring的Validator时,我是否必须从控制器设置pojo,然后进行验证?
devdar 2012年

我不确定我是否理解您的问题。如果看到控制器代码片段,Spring会自动将提交的表单绑定到Foo处理程序方法中的参数。你能澄清一下吗?
stephen.hanson,2012年

好的,我的意思是当用户提交用户输入时,控制器获取http请求,从那里发生的事情是您使用request.getParameter()获取所有用户参数,然后在POJO中设置值,然后传递验证对象的类。验证类会将错误连同发现的错误一起发送回视图。这是这样吗?
devdar 2012年

1
这样会发生,但是有一种更简单的方法...如果您使用JSP和<form:form commandName =“ user”>提交,则数据将自动放入控制器中的@ModelAttribute(“ user”)用户中方法。看到该文档:static.springsource.org/spring/docs/3.0.x/...
杰罗姆Dalbert

+1,因为这是我发现的第一个使用@ModelAttribute的示例;没有它,我发现的所有教程都不起作用。
Riccardo Cossu 2014年

12

我想对杰罗姆·达伯特(Jerome Dalbert)给出很好的回答。我发现以JSR-303的方式编写自己的注释验证器非常容易。您不限于具有“一个字段”验证。您可以在类型级别上创建自己的注释,并进行复杂的验证(请参见下面的示例)。我更喜欢这种方式,因为我不需要像Jerome这样混合使用不同类型的验证(Spring和JSR-303)。另外,此验证器是“ Spring感知的”,因此您可以直接使用@ 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;

}

1
我也想知道一个控制器通常有一个验证器,我看到可以在哪里有多个验证器,但是,如果您为一个对象定义了一组验证,但是要在对象上执行的操作就不同了,例如保存/更新一个对象。保存需要一组特定的验证,并且需要更新一组不同的验证。有没有一种方法可以将验证器类配置为根据操作保存所有验证,还是需要使用多个验证器?
devdar 2013年

1
您还可以对方法进行注释验证。因此,如果我理解您的问题,则可以创建自己的“域验证”。为此,您必须ElementType.METHOD在中指定@Target
michal.kreuzman,2013年

我了解您在说什么,还可以为我提供一个更清晰的图片示例。
devdar

4

如果您对不同的方法处理程序具有相同的错误处理逻辑,那么最终将得到许多具有以下代码模式的处理程序:

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

假设您正在创建RESTful服务,并希望400 Bad Request针对每个验证错误案例返回错误消息。然后,对于每个需要验证的REST端点,错误处理部分将是相同的。在每个处理程序中重复相同的逻辑并不是那么

解决此问题的一种方法是BindingResult在每个要验证的 bean 之后删除立即数。现在,您的处理程序将如下所示:

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

这样,如果绑定的bean无效,MethodArgumentNotValidExceptionSpring将抛出a 。您可以ControllerAdvice使用相同的错误处理逻辑定义处理此异常的:

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

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


1

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

0

将此bean放入配置类中。

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

然后你可以使用

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

用于手动验证bean。然后,您将在BindingResult中获得所有结果,然后可以从那里检索。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.