Spring 09 表单验证与数据预处理

Spring 09 表单验证与数据预处理

前边学习了如何从请求和表单中拿到数据,在增删改查之前还有一个重要的步骤,就是验证表单,如果数据格式不符合要求,那就谈不上增删改查了。 常见的针对表单数据的要求有:必须要填写的字段,数字型字段,特殊格式的字段比如邮件地址和邮政编码,当然还可能有自定义的各种字段。 由于前端的验证可以被绕过,所以后端一定

前边学习了如何从请求和表单中拿到数据,在增删改查之前还有一个重要的步骤,就是验证表单,如果数据格式不符合要求,那就谈不上增删改查了。 常见的针对表单数据的要求有:必须要填写的字段,数字型字段,特殊格式的字段比如邮件地址和邮政编码,当然还可能有自定义的各种字段。 由于前端的验证可以被绕过,所以后端一定要对数据进行验证,Web开发中表单验证是最基本的功能。 Java本身有一套验证Bean的标准API,可以在https://beanvalidation.org/找到。 Spring 4版本及更高版本支持这套验证Bean的标准API,在创建Spring应用的时候就可以将验证系统加入进来,只需要将验证API所在的JAR文件导入进来即可。 Bean的验证API可以验证该字段是否必填,有效的长度,数值,使用正则表达式进行验证,也支持自定义验证等等,在验证的时候使用一些注解,常用的如下:
表单验证注解
注解 解释
@NotNull 不能为空,必须填写
@Min 必须大于等于这个最小值
@Max 必须小于等于这个最大值
@Size 长度
@Pattern 正则表达式验证
@Future/@Past 日期必须是未来/过去的日期

导入Hibernate Validator

Java的Bean验证API(Java标准JSR-303/309)就像JDBC一样,有着不同的具体实现,所以项目里边我们需要一个具体实现。这个时候就该用到Hibernate了。 Hibernate验证器,有着完整的JSR-303/309标准实现,这个验证器不依赖某个具体的ORM或者数据库实现,是一个独立的验证功能,所以用途比较广泛。 可以在http://hibernate.org/validator/里找到该验证器。 在网站上现在有了6.0版的稳定版,将压缩包下载回来,解压缩后看到dist目录下有三个jar,此外lib目录里还有optional和required两个库,其中required也是要添加到库里的。 将dist目录下的三个jar文件加入到项目的库中,然后把required目录下的四个jar文件也拷贝到项目的库目录里(无需创建子目录),这样就把Hibernate Validator导入到了项目中。

必填字段验证

在开始验证之前,依然先看一下总的步骤:
  1. 给Bean添加验证规则
  2. 展示表单给用户
  3. 用户填写表单
  4. 在控制器方法中进行表单验证
  5. 在原页面显示错误信息或者导向成功页面
这里我们新建一个包,然后先来创建一个最简单的Bean类叫做Customer:
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Customer {

    private String firstName;

    @NotNull(message = "is required")
    @Size(min = 1, message = "is required")
    private String lastName;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}
这里的关键是注解修饰lastName变量,第一个@NotNull表示不能为空,message表示错误信息,之后的@Size表示最短为一个字符。这样就对lastName增加了两条验证规则。 之后来编写需要展示的customer-form.jsp:
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
    <title>表单验证</title>
</head>
<body>
<form:form action="/val/processform" modelAttribute="customer">
    First name: <form:input path="firstName" />
    <br><br>
    Last name(*): <form:input path="lastName" />
    <form:errors path="lastName" cssClass="error"/>
    <br><br>
    <input type="submit" value="提交">
</form:form>
</body>
</html>
这里的关键是form:errors标签,用path对应了Custom的同名属性,cssClass指定了基础的显示样式,从名字就能看出来,这个标签将来是要对应这个lastName字段的错误信息。 然后来编写已经驾轻就熟的控制器:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;

@Controller
@RequestMapping(value = "/val")
public class ValidationController {

    @RequestMapping(value = "/form", method = RequestMethod.GET)
    public String show(Model model) {
        model.addAttribute("customer", new Customer());
        return "customer-form";
    }

    @RequestMapping(value = "/processform", method = RequestMethod.GET)
    public String processForm(@Valid @ModelAttribute("customer") Customer customer, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "customer-form";
        } else {
            return "customer-confirmation";
        }
    }
}
控制器中展示表单的方法很简单,给Model直接绑定Customer对象,在processForm方法中,则在原来的绑定Customer对象之前,又加了一个新的@Valid注解,还传入了一个新的BindingResult bindingResult参数,顾名思义,这是一个表单验证后的结果,如果有错误,依然返回表单对象,此时表单对象的值就是用户已经填写的内容,如果没有问题,则将表单数据返回给customer-confirmation.jsp。 BindingResult bindingResult参数必须紧跟在@Valid验证的参数之后,才能够正常接收到验证错误。这一点一定要注意。 再来编写customer-confirmation.jsp,简单展示一下经过验证后的表单数据。 此时运行应用,如果不填写lastName,可以看到旁边出现了错误提示,其元素是<span id="lastName.errors" class="error">is required</span>,有id和class,方便后期做样式。

数字验证及配置自定义错误信息

先从最简单的数字验证开始,创建一个字段输入,比如限制从1-10,先给Customer增加一个字段叫做freePasses,配置好getter和setter:
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Customer {

    private String firstName;

    @NotNull(message = "is required")
    @Size(min = 1, message = "is required")
    private String lastName;

    @Min(value=0,message = "must be greater than or equal to 0")
    @Max(value=10,message = "must be less than or equal to 10")
    private int freePasses;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getFreePasses() {
        return freePasses;
    }

    public void setFreePasses(int freePasses) {
        this.freePasses = freePasses;
    }
}
这里我们使用了@Min@Max两个验证规则,value表示值,message表示错误信息。 控制器方法是不用做任何修改的,因为我们绑定了Customer对象,而且配置好了对象验证。在表单里增加一个input框,结果里增加一个输出:
Free passes: <form:input path="freePasses"/>
<form:errors path="freePasses" cssClass="error"></form:errors>
<p>Your free passes is ${customer.freePasses}</p>
可以看到,输入数字超过0-10范围的时候很就可以显示错误信息 但是很快就可以发现,如果输入字符或者不输入,或者输入空白,都会报错:
Failed to convert property value of type java.lang.String to required type int for property freePasses;
nested exception is java.lang.NumberFormatException: For input string: "XXXX"
可见仅仅配置@Min和@Max是不行的。很显然,这是试图将字符串转换为int基本类型的时候出错,由于没有这个验证规则对应的错误,默认会显示Java的错误信息。 其实这个问题的解决方式有很多,由于接受的数据最初都是字符串,所以可以用正则表达式。也可以单独配置不为空,再使用自定义错误信息覆盖默认错误信息。 由于正则表达式会单独学习,这里来采用单独配置不为空,再使用自定义错误信息的方法。 首先解决不为空的问题,由于空字符串会被解析成null,而int基本类型无法接收null,因此可以将字段类型改为包装类型Integer,来修改一下Customer类:
@NotNull(message = "is required")
@Min(value=0,message = "must be greater than or equal to 0")
@Max(value=10,message = "must be less than or equal to 10")
private Integer freePasses;
这里增添了@NotNull,修改了变量类型,注意getter和setter方法也要相应修改。 再启动项目,试验一下什么都不填,可以正确的显示is required了。 但是依然没有解决输入字符显示的错误信息,一般数字解析错误,我们无需向用户展示解析错误,只要说无效的输入就可以了,这里可以配置默认错误信息。 为了先了解错误信息,可以在控制器方法内,打印一下BindingResult bindingResult对象,在输入字符之后,可以发现:
BindingResult: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'customer' on field 'freePasses': rejected value [fas]; codes [typeMismatch.customer.freePasses,typeMismatch.freePasses,typeMismatch.java.lang.Integer,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customer.freePasses,freePasses]; arguments [];
default message [freePasses]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Integer' for property 'freePasses'; nested exception is java.lang.NumberFormatException: For input string: "fas"]
第一行里会告诉你验证不通过的输入内容是"fas",错误类型是typeMismatch.customer.freePasses,最后一行默认信息就是显示出来的信息。

自定义错误信息

自定义错误信息的规定比较严格,要按照如下步骤操作:
  1. src/resources/目录下创建一个包含自定义错误信息文件customerror.properties(名字可以任意起,但路径要放对)
  2. 在Spring配置中载入这个自定义文件
先来创建这个自定义错误文件customerror.properties:
typeMismatch.customer.freePasses=Invalid Number
然后在配置文件中将其载入为一个Bean:
<bean id="messageSource"
      class="org.springframework.context.support.ResourceBundleMessageSource"   >
    <property name="basenames" value="resources/customerror"></property>
</bean>
这个Bean是特殊的Bean,id必须为"messageSource",不能为其他名称,否则自定义错误信息无效。另外property标签中的name="basenames"也不能任意指定,value中的customerror省略了.properties后缀名。 也就是说自定义错误信息除了文件名可以自定义,错误类型可以通过打印BindingResult对象查看,其他的Bean的属性都不能自行修改。 默认错误信息配置好以后,相当于一个托底的信息,当错误不满足其他验证条件的时候,就会显示默认信息。

正则表达式验证

正则表达式验证使用@Pattern注解,再添加一个字段,想用其输入5位邮政编码,只能是大小写字母和数字:
@NotNull(message = "is required")
@Pattern(regexp = "^[a-zA-Z0-9]{5}", message = "Invalid Postal Code")
private String postCode;
这里regexp就是正则表达式,注意Java的正则表达式需要转义,这里的规则是匹配5个数字或字母。getter和setter方法以及jsp的模板代码省略。 这里在自己尝试的时候,还发现正则表达式无法匹配空,所以要加一个@NotNull。 正则表达式其实非常灵活,一般搭配@NotNull和之前设置的自动进行去空白操作,就可以将数据清洗为所需格式,然后再使用其他验证方式。

预先处理数据

用户在text及类似类型的表单控件中如果不输入任何字符,返回给后端的是一个空字符串。空字符串不会被当成null来进行验证。 但是空字符串如果被Trim过,就会变成null。所以可以添加一个绑定数据之前的预先处理的控制器方法,这个方法用@InitBinder来注解,有很多特殊的预处理器:
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
    dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
}
这个就是特殊的预处理方法,使用的都是特殊的对象,这里相当于一个一个过滤开关,设置了trim过滤为true,然后将这个过滤器注册给所有的String对象。 在需要使用这个方法的控制器类中添加上这样一个方法之后,所有的这个控制器里的数据绑定都会预先经过这个处理。当然也可以用正则表达式,就不用预处理了。
LICENSED UNDER CC BY-NC-SA 4.0
Comment