简体   繁体   English

Spring MVC:如何执行验证?

[英]Spring MVC: How to perform validation?

I would like to know what is the cleanest and best way to perform form validation of user inputs.我想知道执行用户输入表单验证的最干净和最好的方法是什么。 I have seen some developers implement org.springframework.validation.Validator .我见过一些开发人员实现org.springframework.validation.Validator A question about that: I saw it validates a class.关于这个问题:我看到它验证了一个类。 Does the class have to be filled manually with the values from the user input, and then passed to the validator?是否必须使用来自用户输入的值手动填充该类,然后传递给验证器?

I am confused about the cleanest and best way to validate the user input.我对验证用户输入的最干净和最好的方法感到困惑。 I know about the traditional method of using request.getParameter() and then manually checking for nulls , but I don't want to do all the validation in my Controller .我知道使用request.getParameter()然后手动检查nulls的传统方法,但我不想在我的Controller进行所有验证。 Some good advice on this area will be greatly appreciated.在这方面的一些好的建议将不胜感激。 I am not using Hibernate in this application.我没有在这个应用程序中使用 Hibernate。

With Spring MVC, there are 3 different ways to perform validation : using annotation, manually, or a mix of both.使用 Spring MVC,有 3 种不同的方式来执行验证:使用注解、手动或两者的混合。 There is not a unique "cleanest and best way" to validate, but there is probably one that fits your project/problem/context better.没有一种独特的“最干净和最好的方法”来验证,但可能有一种更适合您的项目/问题/上下文。

Let's have a User :让我们有一个用户:

public class User {

    private String name;

    ...

}

Method 1 : If you have Spring 3.x+ and simple validation to do, use javax.validation.constraints annotations (also known as JSR-303 annotations).方法 1:如果您有 Spring 3.x+ 和简单的验证要做,请使用javax.validation.constraints注释(也称为 JSR-303 注释)。

public class User {

    @NotNull
    private String name;

    ...

}

You will need a JSR-303 provider in your libraries, like Hibernate Validator who is the reference implementation (this library has nothing to do with databases and relational mapping, it just does validation :-).您的库中将需要一个 JSR-303 提供程序,例如作为参考实现的Hibernate Validator (该库与数据库和关系映射无关,它只进行验证 :-)。

Then in your controller you would have something like :然后在你的控制器中你会有类似的东西:

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

Notice the @Valid : if the user happens to have a null name, result.hasErrors() will be true.请注意 @Valid :如果用户碰巧有一个空名称,则 result.hasErrors() 将为真。

Method 2 : If you have complex validation (like big business validation logic, conditional validation across multiple fields, etc.), or for some reason you cannot use method 1, use manual validation.方法 2:如果您有复杂的验证(如大型企业验证逻辑、跨多个字段的条件验证等),或者由于某些原因无法使用方法 1,请使用手动验证。 It is a good practice to separate the controller's code from the validation logic.将控制器的代码与验证逻辑分开是一种很好的做法。 Don't create your validation class(es) from scratch, Spring provides a handy org.springframework.validation.Validator interface (since Spring 2).不要从头开始创建验证类,Spring 提供了一个方便的org.springframework.validation.Validator接口(自 Spring 2 起)。

So let's say you have所以让我们说你有

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

and you want to do some "complex" validation like : if the user's age is under 18, responsibleUser must not be null and responsibleUser's age must be over 21.并且您想要进行一些“复杂”的验证,例如:如果用户的年龄未满 18 岁,则负责用户不得为空且负责用户的年龄必须超过 21。

You will do something like this你会做这样的事情

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

    }

}

Then in your controller you would have :然后在您的控制器中,您将拥有:

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

If there are validation errors, result.hasErrors() will be true.如果存在验证错误,则 result.hasErrors() 将为 true。

Note : You can also set the validator in a @InitBinder method of the controller, with "binder.setValidator(...)" (in which case a mix use of method 1 and 2 would not be possible, because you replace the default validator).注意:您还可以在控制器的 @InitBinder 方法中设置验证器,使用“binder.setValidator(...)”(在这种情况下,方法 1 和 2 的混合使用是不可能的,因为您替换了默认值验证器)。 Or you could instantiate it in the default constructor of the controller.或者您可以在控制器的默认构造函数中实例化它。 Or have a @Component/@Service UserValidator that you inject (@Autowired) in your controller : very useful, because most validators are singletons + unit test mocking becomes easier + your validator could call other Spring components.或者在控制器中注入一个 @Component/@Service UserValidator(@Autowired):非常有用,因为大多数验证器都是单例 + 单元测试模拟变得更容易 + 你的验证器可以调用其他 Spring 组件。

Method 3 : Why not using a combination of both methods?方法 3:为什么不结合使用这两种方法? Validate the simple stuff, like the "name" attribute, with annotations (it is quick to do, concise and more readable).使用注释验证简单的东西,例如“名称”属性(这样做很快,简洁且更具可读性)。 Keep the heavy validations for validators (when it would take hours to code custom complex validation annotations, or just when it is not possible to use annotations).保留对验证器的大量验证(当编写自定义复杂验证注释需要数小时,或者仅当无法使用注释时)。 I did this on a former project, it worked like a charm, quick & easy.我在以前的一个项目上做过这个,它的作用就像一个魅力,快速而简单。

Warning : you must not mistake validation handling for exception handling .警告:您不能将验证处理误认为是异常处理 Read this post to know when to use them.阅读这篇文章以了解何时使用它们。

References :参考 :

There are two ways to validate user input: annotations and by inheriting Spring's Validator class.有两种方法可以验证用户输入:注释和继承 Spring 的 Validator 类。 For simple cases, the annotations are nice.对于简单的情况,注释很好。 If you need complex validations (like cross-field validation, eg. "verify email address" field), or if your model is validated in multiple places in your application with different rules, or if you don't have the ability to modify your model object by placing annotations on it, Spring's inheritance-based Validator is the way to go.如果您需要复杂的验证(例如跨字段验证,例如“验证电子邮件地址”字段),或者如果您的模型在应用程序中的多个位置使用不同的规则进行验证,或者您无法修改您的通过在模型对象上放置注释,Spring 的基于继承的 Validator 是要走的路。 I'll show examples of both.我将展示两者的例子。

The actual validation part is the same regardless of which type of validation you're using:无论您使用哪种类型的验证,实际的验证部分都是相同的:

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

If you are using annotations, your Foo class might look like:如果您使用注释,您的Foo类可能如下所示:

public class Foo {

    @NotNull
    @Size(min = 1, max = 20)
    private String name;

    @NotNull
    @Min(1)
    @Max(110)
    private Integer age;

    // getters, setters
}

Annotations above are javax.validation.constraints annotations.上面的注释是javax.validation.constraints注释。 You can also use Hibernate's org.hibernate.validator.constraints , but it doesn't look like you are using Hibernate.您也可以使用 Hibernate 的org.hibernate.validator.constraints ,但它看起来不像您正在使用 Hibernate。

Alternatively, if you implement Spring's Validator, you would create a class as follows:或者,如果您实现 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]");
        }
    }
}

If using the above validator, you also have to bind the validator to the Spring controller (not necessary if using annotations):如果使用上述验证器,您还必须将验证器绑定到 Spring 控制器(如果使用注解则不需要):

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

Also see Spring docs .另请参阅Spring 文档

Hope that helps.希望有帮助。

I would like to extend nice answer of Jerome Dalbert.我想扩展 Jerome Dalbert 的好回答。 I found very easy to write your own annotation validators in JSR-303 way.我发现以 JSR-303 方式编写自己的注释验证器非常容易。 You are not limited to have "one field" validation.您不仅限于“单一领域”验证。 You can create your own annotation on type level and have complex validation (see examples below).您可以在类型级别创建自己的注释并进行复杂的验证(请参见下面的示例)。 I prefer this way because I don't need mix different types of validation (Spring and JSR-303) like Jerome do.我更喜欢这种方式,因为我不需要像 Jerome 那样混合不同类型的验证(Spring 和 JSR-303)。 Also this validators are "Spring aware" so you can use @Inject/@Autowire out of box.此外,此验证器具有“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;

}

If you have same error handling logic for different method handlers, then you would end up with lots of handlers with following code pattern:如果您对不同的方法处理程序有相同的错误处理逻辑,那么您最终会得到许多具有以下代码模式的处理程序:

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

Suppose you're creating RESTful services and want to return 400 Bad Request along with error messages for every validation error case.假设您正在创建 RESTful 服务并希望为每个验证错误案例返回400 Bad Request以及错误消息。 Then, the error handling part would be same for every single REST endpoint that requires validation.然后,每个需要验证的 REST 端点的错误处理部分都是相同的。 Repeating that very same logic in every single handler is not so DRY ish!重复说同样的逻辑在每一个处理程序是不是这么的ISH!

One way to solve this problem is to drop the immediate BindingResult after each To-Be-Validated bean.解决此问题的一种方法是在每个To-Be-Validated bean 之后立即BindingResult Now, your handler would be like this:现在,您的处理程序将是这样的:

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

This way, if the bound bean was not valid, a MethodArgumentNotValidException will be thrown by Spring.这样,如果绑定的 bean 无效, MethodArgumentNotValidException将抛出MethodArgumentNotValidException You can define a ControllerAdvice that handles this exception with that same error handling logic:您可以定义一个ControllerAdvice来使用相同的错误处理逻辑处理此异常:

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

You still can examine the underlying BindingResult using getBindingResult method of MethodArgumentNotValidException .您仍然可以使用BindingResult getBindingResult方法检查底层MethodArgumentNotValidException

Find complete example of Spring Mvc Validation查找 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);
    }
}

Put this bean in your configuration class.将此 bean 放在您的配置类中。

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

and then You can use然后你可以使用

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

for validating a bean manually.用于手动验证 bean。 Then You will get all result in BindingResult and you can retrieve from there.然后您将在 BindingResult 中获得所有结果,您可以从那里检索。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM