java Spring MVC:如何执行验证?

envsm3lx  于 2023-02-18  发布在  Java
关注(0)|答案(7)|浏览(127)

我想知道对用户输入执行表单验证的最干净和最好的方法是什么。我见过一些开发人员实现org.springframework.validation.Validator。关于这个问题有一个问题:我看到它验证了一个类。这个类是否必须手工填充用户输入的值,然后传递给验证器?
我对验证用户输入的最干净和最好的方法感到困惑。我知道使用request.getParameter()然后手动检查nulls的传统方法,但我不想在我的Controller中做所有的验证。如果能在这方面提供一些好的建议,我将不胜感激。我在这个应用程序中没有使用Hibernate。

x6492ojm

x6492ojm1#

使用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,它是参考实现(这个库与数据库和关系Map无关,它只进行验证:-)。
那么在你的控制器中,你会有这样的东西:

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

    • 方法3:**为什么不结合使用这两种方法呢?用注解来验证简单的东西,比如"name"属性(这样做很快,简洁,可读性更强)。把繁重的验证留给验证器(当编写自定义复杂的验证注解需要花费几个小时,或者只是不可能使用注解时)。我在以前的一个项目中做过这个,它就像一个魅力,快速而简单。
    • 警告:您一定不要将验证处理误认为异常处理**. Read this post,以便知道何时使用它们。

参考文献:

uxhixvfz

uxhixvfz2#

验证用户输入的方法有两种:注解和继承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文档。
希望能有所帮助。

kqqjbcuj

kqqjbcuj3#

我想扩展一下杰罗姆的回答。我发现用JSR-303的方式编写自己的注解验证器非常容易。您不限于“一个字段”验证。您可以在类型级别创建自己的注解并进行复杂的验证(参见下面的示例)。我更喜欢这种方式,因为我不需要混合不同类型的验证(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;

}
fiei3ece

fiei3ece4#

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

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

假设您正在创建RESTful服务,并希望返回400 Bad Request沿着每个验证错误情况的错误消息,那么,对于每个需要验证的REST端点,错误处理部分都是相同的,在每个处理程序中重复相同的逻辑并不那么枯燥!
解决这个问题的一个方法是在每个 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

yhived7q

yhived7q5#

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

3yhwsihp6#

将这个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中得到所有结果,并且你可以从那里检索。

biswetbf

biswetbf7#

确认组

同样值得一提的是,在一些更复杂的情况下,当你的业务逻辑中有一些“多步骤”时,我们需要“验证组”。
添加了@Validated注解以支持validated bean中的“validation groups”。这可以用于多步骤表单,其中第一步需要验证姓名和电子邮件,第二步需要验证电话号码。
使用@Validated时,首先需要声明组,组是使用自定义标记接口声明的。

@确认示例

假设我们有一个场景,我们有一个表单供用户注册。在这个表单上,我们希望用户提供姓名和电子邮件。用户注册后,我们有另一个表单,我们建议用户添加一些额外的信息,例如,电子邮件。我们不希望在第一步提供电子邮件。但要求在第二步提供它。
对于这个例子,我们将声明两个组,第一个组是OnCreate,第二个组是OnUpdate

创建时

public interface OnCreate {}

更新时

public interface OnUpdate {}

我们的用户UserAccount类:

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   
    
}

我们用组接口标记验证注解,这取决于那些验证应该与哪个组相关。
最后是我们的Controller方法:

@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,并指定应在不同情况下使用的验证组。
现在,根据验证组,我们将在不同的步骤中对特定字段执行验证。

相关问题