【问题标题】:How to perform custom validations in Spring MVC?如何在 Spring MVC 中执行自定义验证?
【发布时间】:2021-03-19 00:46:58
【问题描述】:

我有以下代码允许用户更新姓名和年份。

型号

@Entity
public class Person implements Serializable{

    private static final long serialVersionUID = 1L;

    private String name;

    private int year;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

}

控制器

@RequestMapping(value = "/person", method = RequestMethod.POST)
public ModelAndView editPerson(@ModelAttribute Person person)
{
    //perform operations

    //display results
    ModelAndView modelAndView = new ModelAndView("Person.html");
    modelAndView.addObject("personBind", person);
        
    return modelAndView;

}

查看

<form action="person" method="post" th:object="${personBind}">
Name:
<input type="text" th:field="*{name}" />

Year:   
<input type="text" th:field="*{year}" />

现在我想在年份字段中进行一些验证。 例如,如果用户在该字段中输入字符串而不是数字,则当前代码将抛出异常,因为它不允许在整数属性中设置字符串。

那么如何验证输入呢? 不想使用@Valid。想做一些自定义验证。

我发现这样做的方法是在模型中创建年份字段的字符串版本(getter/setter)。然后在视图中使用该 strYear 并在控制器中进行验证。就像下面更新的代码一样。这是正确的方法还是有更好的方法来做到这一点?我问是因为不确定为每个需要验证的数字属性创建字符串版本的 getter/setter 是否正确。似乎有很多重复。

型号

@Entity
public class Person implements Serializable{

    private static final long serialVersionUID = 1L;

    private String name;

    private int year;

    @Transient
    private String strYear;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }    
    
    public String getStrYear() {
        return strYear;
    }

    public void setStrYear(String strYear) {
        this.strYear = strYear;
    }

}

控制器

@RequestMapping(value = "/person", method = RequestMethod.POST)
public ModelAndView editPerson(@ModelAttribute Person person)
{
    //validate
    boolean valid = Validate(Person.getStrYear());

    if(valid==true)
    {
      Person.setYear(Integer.ParseInt(Person.getStrYear()));
      //save edit
    }
    else
    {//display validation messages}

    //display results
    ModelAndView modelAndView = new ModelAndView("Person.html");
    modelAndView.addObject("personBind", person);
        
    return modelAndView;

}

查看

<form action="person" method="post" th:object="${personBind}">
Name:
<input type="text" th:field="*{name}" />

Year:   
<input type="text" th:field="*{strYear}" />

【问题讨论】:

  • 不想使用@Valid注解是什么想法?因为,这通常是我们在 Spring MVC 中进行验证的方式。
  • 几个原因。 1)我的表单中有一些不在模型实体中的字段。了解@Valid 无法验证。所以想法会以相同的方式进行所有验证,而不是与@Valid 分开并以不同的方式分开。 2)不确定如何使用@Valid 执行自定义验证(假设我需要执行计算以说明输入是否正确,甚至检查是否在我发布的这个问题中的数字字段中输入了字符串)。可能是这样,只是我不知道。
  • 验证可能很棘手,但您提出的所有问题都可以通过 @Valid 注释解决。我会一起为您解答,但需要一些时间才能正确写出。
  • baeldung.com/spring-mvc-custom-validator 尝试自定义验证器
  • @hooknc 我在使用@Valid 时遇到的另一个问题是我在这个问题上发布的问题:stackoverflow.com/questions/65190605/…

标签: java spring-mvc thymeleaf


【解决方案1】:

验证可能很棘手也很困难,在进行验证时需要考虑一些事项...

验证注意事项

  • MVC 中的模型(模型、视图、控制器)与您的领域模型不同,通常也不应该相同。请参阅@wim-deblauwe 的评论和下面的问答部分。

    • 通常,用户界面中显示的内容与域模型中显示的内容不同。
    • 将@Valid 注释放入域模型意味着在使用域模型的每种形式中,都将应用相同的@Valid 规则。这并非总是如此。旁注:这可能不适用于超级简单的 CRUD(创建、读取、更新、删除)应用程序,但总的来说,大多数应用程序比纯 CRUD 更复杂。
  • 由于 Spring 在表单提交期间自动设置值的方式,使用真正的域模型对象作为表单支持对象存在严重的安全问题。例如,如果我们使用带有密码字段的用户对象作为表单支持对象,则浏览器开发人员工具可以操作表单以发送密码字段的新值,现在新值将被持久化。

  • 通过 html 表单输入的所有数据实际上都是字符串数据,稍后需要将其转置为实际数据类型(整数、双精度、枚举等)。

  • 在我看来,有不同类型的验证需要以不同的时间顺序进行。

    • 必需的检查发生在类型检查(整数、双精度、枚举等)、有效值范围之前,最后是持久性检查(唯一性、以前的持久值等...)
    • 如果时间级别有任何错误,那么以后不要检查任何内容。
      • 这可以防止最终用户在同一条错误消息中收到错误消息,例如需要电话号码、电话号码不是数字、电话号码格式不正确等。
  • 验证器之间不应存在任何时间耦合。这意味着如果一个字段是可选的,那么如果一个值不存在,那么“数据类型”验证器不应该验证失败。请参阅下面的验证器。

示例

域对象/业务对象:

@Entity
public class Person {

    private String identifier;
    private String name;
    private int year;

    public String getIdentifier() {
        return identifier;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }    
}

要通过 Spring MVC 控制器填充 html 表单,我们将创建一个表示该表单的特定对象。这也包括所有验证规则:

@GroupSequence({Required.class, Type.class, Data.class, Persistence.class, CreateOrUpdatePersonForm.class})
public class CreateOrUpdatePersonForm {

    @NotBlank(groups = Required.class, message = "Name is required.")
    private String name;

    @NotBlank(groups = Required.class, message = "Year is required.")
    @ValidInteger(groups = Type.class, message = "Year must be a number.")
    @ValidDate(groups = Data.class, message = "Year must be formatted yyyy.")
    private String year;

    public CreateOrUpdatePersonForm(Person person) {
        this.name = person.getName();
        this.year = Integer.valueOf(person.getYear);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getYearStr() {
        this.year;
    }

    public void setYearStr(String year) {
        this.year = year;
    }

    public int getYear() {
        return Integer.valueOf(this.year);
    }
}

然后在你的控制器中使用新的 CreateOrUpdatePersonForm 对象:

@Controller
public class PersonController {
    ...
    @ModelAttribute("command")
    public CreateOrUpdatePersonForm setupCommand(@RequestParam("identifier") Person person) {

        return new CreateOrUpdatePersonForm(person);
    }

    //@PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.GET)
    public ModelAndView getForm(@RequestParam("person") Person person) {

        return new ModelAndView("/form/person");
    }

    //@PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.POST)
    public ModelAndView postForm(@RequestParam("person") Person person, @ModelAttribute("command") @Valid CreateOrUpdatePersonForm form,
                                 BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        ModelAndView modelAndView;

        if (bindingResult.hasErrors()) {

            modelAndView = new ModelAndView("/form/person");

        } else {

            this.personService.updatePerson(person.getIdentifier(), form);

            redirectAttributes.addFlashAttribute("successMessage", "Person updated.");

            modelAndView = new ModelAndView("redirect:/person/" + person.getIdentifier() + ".html");
        }

        return modelAndView;
    }
}

@ValidInteger 和 @ValidDate 是我们自己编写的验证器。

@ValidInteger:

public class ValidIntegerValidator implements ConstraintValidator<ValidInteger, String> {

    @Override
    public void initialize(ValidInteger annotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {

        boolean valid = true;

        if (StringUtils.hasText(value)) {

            try {
                Integer.parseInteger
(value);

            } catch (NumberFormatException e) {

                valid = false;
            }
        }

        return valid;
    }
}

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidIntegerValidator.class)
@Documented
public @interface ValidInteger {

    String message() default "{package.valid.integer}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

@ValidDate

public class ValidDateValidator implements ConstraintValidator<ValidDate, String> {

    private String format;

    @Override
    public void initialize(ValidDate annotation) {
        this.format = annotation.format();
    }

    @Override
    public boolean isValid(String inputDate, ConstraintValidatorContext constraintValidatorContext) {

        boolean valid = true;

        if (StringUtils.hasText(inputDate)) {

            SimpleDateFormat dateFormat = new SimpleDateFormat(format);

            dateFormat.setLenient(false);

            try {

                dateFormat.parse(inputDate);

            } catch (ParseException e) {

                valid = false;
            }
        }

        return valid;
    }
}

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidDateValidator.class)
@Documented
public @interface ValidDate {

    String message() default "{package.dateformat}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String format();
}

然后在您的视图 jsp 或模板中,如果有任何错误,您将需要显示:

<html>
    ...
    <body>
        <common:form-errors modelAttribute="command"/>
        ...
    </body>
</html>

还有很多需要处理的验证,比如比较两个字段,或者访问您的持久层以验证人名是否唯一,但这需要更多解释。

问答

问:您能否提供链接来解释不使用域模型作为 MVC 模型背后的想法?

答:当然,Entities VS Domain Models VS View ModelsEntity vs Model vs View Model

TL;DR:对域模型和 MVC 模型使用不同的对象是因为它减少了应用程序层之间的耦合,并保护我们的 UI 和域模型免受任一层的更改。

其他注意事项

数据验证需要在应用程序的所有入口点进行:UI、API 以及读入的任何外部系统或文件。

API 只是计算机的 UI,需要遵循与人类 UI 相同的规则。

接受来自互联网的数据充满危险。限制比限制少要好。这还包括确保没有任何奇怪的字符 coughMicrosoft's 1252 character encodingcough、Sql 注入、JavaScript 注入,确保您的数据库设置为 unicode 和理解由于代码点,为 512 个字符设置的列(取决于语言)实际上只能处理 256 个字符。

【讨论】:

  • 非常详细的答案。这很有帮助。基于此,我看到我需要做一些更改。非常感谢您花费时间和精力将这些放在一起。非常感谢。
  • 谢谢,很高兴它有用。
  • 你是否有一些链接可以指出更多关于这种设计的有一个代表表单的类的信息?想了解更多。我看到的所有示例都将模型直接绑定到表单,中间没有这个表单类。但从我的经验来看,拥有它是很有意义的。
  • 我会看,没有很多示例显示这一点,因为仅使用域模型类“更容易”,我觉得这就是大多数示例这样做的原因。事实上,在我正在处理的项目中,我们将域对象用于非基于表单的视图。但是对于我们的表单,我们总是使用专门表示表单的类。
  • @jkfe 既然你明确要求资源:我在我的书Taming Thymeleaf 第 11 章的表格中解释了这一点。您可以在github.com/wimdeblauwe/taming-thymeleaf-sources免费查看示例的来源。
猜你喜欢
  • 2012-08-22
  • 2014-09-24
  • 1970-01-01
  • 1970-01-01
  • 2020-04-24
  • 1970-01-01
  • 2011-06-13
  • 2019-04-21
相关资源
最近更新 更多