Spring MVC:如何执行验证



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

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

使用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验证器,它是参考实现(这个库与数据库和关系映射无关,它只是做验证:-(。

然后在您的控制器中,您将拥有类似以下内容:

@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 岁,则 Responsible 用户不得为 null,并且 Responsible 用户的年龄必须超过 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。

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

方法 3 :为什么不使用这两种方法的组合?用注释验证简单的东西,如"name"属性(它做起来很快,简洁且更具可读性(。保留对验证程序的繁重验证(当编写自定义复杂验证注释需要数小时时,或者只是在无法使用注释时(。我在以前的一个项目中做了这个,它就像一个魅力,快速而简单。

警告:您不得将验证处理误认为异常处理。阅读这篇文章以了解何时使用它们。

引用:

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

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

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

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,但看起来您不像在使用休眠。

或者,如果你实现了 Spring 的验证器,你可以创建一个类,如下所示:

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 文档。

希望有帮助。

我想

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

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

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

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

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

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

这样,如果绑定的豆子无效,春天就会抛出MethodArgumentNotValidException。您可以定义一个ControllerAdvice,该使用相同的错误处理逻辑来处理此异常:

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

您仍然可以使用getBindingResult MethodArgumentNotValidException方法检查基础BindingResult

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

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

验证组

此外,值得一提的是,当您的业务逻辑中有一些"多步骤"时,对于一些更复杂的情况进行验证。在这种情况下,我们需要"验证组"。

添加了@Validated注释以支持已验证 Bean 中的"验证组"。这可以在多步骤表单中使用,其中第一步需要验证姓名和电子邮件,在第二步中需要验证电话号码。

使用@Validated,您首先需要声明组。组是使用自定义标记接口声明的。

<小时 />

@Validated示例

假设我们有一个用于用户注册的表单。在此表单上,我们希望用户提供姓名和电子邮件。用户注册后,我们还有另一个表单,我们建议用户添加一些额外的信息,例如电子邮件。我们不希望在第一步中发送电子邮件。但需要在第二步提供它。

对于这种情况,我们将声明两个组。第一组是OnCreate,第二组是OnUpdate

OnCreate

public interface OnCreate {}

更新中

public interface OnUpdate {}

我们的用户用户帐户类:

public class UserAccount {
    // we will return this field after User is created
    // and we want this field to be provided only on update
    // so we can determine which user needs to be updated
    @NotBlank(groups = OnUpdate.class)
    private String id;
    @NotBlank(groups = OnCreate.class)
    private String name;
   
    @NotBlank(groups = OnCreate.class)
    private String email;
 
    @NotBlank(groups = OnUpdate.class)
    private String phone;
    
    // standard constructors / setters / getters / toString   
    
}

我们使用组接口标记验证注释,具体取决于这些验证应该与哪个组相关。

最后是我们的控制器方法:

@PostMapping(value = "/create")
public UserAccount createAccount(@Validated(OnCreate.class) @RequestBody UserAccount userAccount) {
    ...
}
@PatchMapping(value = "/update")
public UserAccount updateAccount(@Validated(OnUpdate.class) @RequestBody UserAccount userAccount) {
    ...
}

在这里,我们指定@Validated(...)而不是@Valid,并指定应在不同情况下使用的验证组。

现在,根据验证组,我们将在不同的步骤中对特定字段执行验证。

相关内容

  • 没有找到相关文章

最新更新