首页 文章

Spring MVC:如何执行验证?

提问于
浏览
140

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

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

6 回答

  • 302

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

    让我们有一个用户:

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

    Method 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 .

    Method 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的混合使用,因为您替换了默认值验证器) . 或者您可以在控制器的默认构造函数中实例化它 . 或者在控制器中注入一个@Component / @Service UserValidator(@Autowired):非常有用,因为大多数验证器都是单例测试模拟变得更容易验证器可以调用其他Spring组件 .

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

    Warning : 你不能将 validation handling 误认为 exception handling . Read this post知道何时使用它们 .

    参考文献:

  • 3

    有两种方法可以验证用户输入:注释和继承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 .

    希望有所帮助 .

  • 0

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

    Example of custom object 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 {
    }
    

    Example of generic fields equality:

    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

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

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

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

    解决此问题的一种方法是在每个To-Be-Validated 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 .

  • 12

    查找Spring Mvc Validation的完整示例

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

    将此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中的所有结果,你可以从那里检索 .

相关问题