【问题标题】:Spring MVC: How to perform validation?Spring MVC:如何执行验证?
【发布时间】:2012-08-22 04:42:08
【问题描述】:

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

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

【问题讨论】:

标签: java spring-mvc validation user-input


【解决方案1】:

使用 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() 将为真。

方法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() 将为真。

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

方法三: 为什么不结合使用这两种方法呢?使用注释验证简单的东西,例如“名称”属性(它做起来很快,简洁且更具可读性)。为验证器保留繁重的验证(当编写自定义复杂验证注释需要数小时,或者只是在无法使用注释时)。我在以前的一个项目中这样做过,它的效果非常好,快速且简单。

警告:您不能将验证处理误认为异常处理Read this post 知道何时使用它们。

参考资料:

【讨论】:

  • 你能告诉我我的 servlet.xml 对于这个配置应该有什么吗?我想将错误传递回视图
  • @dev_darin 你的意思是 JSR-303 验证的配置?
  • @dev_marin 对于验证,在 Spring 3.x+ 中,“servlet.xml”或“[servlet-name]-servlet.xml 没有什么特别的。你只需要在你的 hibernate-validator jar项目库(或通过 Maven)。仅此而已,它应该可以工作。警告如果您使用方法 3:默认情况下,每个控制器都可以访问 JSR-303 验证器,因此请注意不要用“setValidator”覆盖它。如果你想在上面添加一个自定义验证器,只需实例化并使用它,或者注入它(如果它是 Spring 组件)。如果在检查 google 和 Spring doc 后仍然有问题,你应该发布一个新问题。跨度>
  • 对于方法1和2的混合使用,有一种使用@InitBinder的方法。代替“binder.setValidator(...)”,可以使用“binder.addValidators(...)”
  • 如果我错了,请纠正我,但是您可以在使用 @InitBinder 注释时通过 JSR-303 注释(方法 1)和自定义验证(方法 2)混合验证。只需使用 binder.addValidators(userValidator) 而不是 binder.setValidator(userValidator) 两种验证方法都会生效。
【解决方案2】:

有两种方法可以验证用户输入:注解和继承 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 的验证器时,我是否必须从控制器设置 pojo,然后验证它?
  • 我不确定我是否理解您的问题。如果看到控制器代码 sn -p,Spring 会自动将提交的表单绑定到处理程序方法中的Foo 参数。你能澄清一下吗?
  • 好的,我的意思是当用户提交用户输入时,控制器获取 http 请求,然后您使用 request.getParameter() 获取所有用户参数在 POJO 中设置值,然后将类传递给验证对象。如果发现任何错误,验证类会将错误连同错误一起发送回视图。是这样的吗?
  • 会这样,但是有一个更简单的方法...如果你使用 JSP 和
    提交,数据会自动放入 @ModelAttribute( "user") 控制器方法中的用户。请参阅文档:static.springsource.org/spring/docs/3.0.x/…
  • +1 因为这是我发现的第一个使用@ModelAttribute 的示例;没有它,我发现的教程都不起作用。
【解决方案3】:

我想扩展 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;

}

【讨论】:

  • 我还想知道控制器通常有一个验证器,我看到你可以在哪里有多个验证器但是如果你为一个对象定义了一组验证但是你想要在对象上执行的操作是不同的,例如保存/更新需要一组验证,而更新需要一组不同的验证。有没有办法配置验证器类来保存基于操作的所有验证,或者您是否需要使用多个验证器?
  • 您还可以对方法进行注释验证。因此,如果我理解您的问题,您可以创建自己的“域验证”。为此,您必须在 @Target 中指定 ElementType.METHOD
  • 我明白你在说什么,你能不能给我举个例子让我看得更清楚。
【解决方案4】:

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

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

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

解决此问题的一种方法是在每个待验证 bean 之后立即删除BindingResult。现在,您的处理程序将是这样的:

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

这样,如果绑定的 bean 无效,Spring 将抛出 MethodArgumentNotValidException。你可以定义一个ControllerAdvice,用同样的错误处理逻辑来处理这个异常:

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

您仍然可以使用MethodArgumentNotValidExceptiongetBindingResult 方法检查底层BindingResult

【讨论】:

    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

      把这个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 中获得所有结果,您可以从那里检索。

      【讨论】:

        猜你喜欢
        • 2021-03-19
        • 1970-01-01
        • 2023-03-05
        • 2020-11-18
        • 2013-03-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多