SpringMVC 数据绑定&数据格式化&数据校验

数据绑定流程

  1. Spring MVC将ServletRequest对象及目标方法的入参实例传给WebDataBinderFactory实例,创建出DataBinder(数据绑定的核心部件)
  2. DataBinder调用转配在SpringMVC上下文中的ConversionService组件进行数据类型转换、数据格式化。并将servlet中的请求信息填充到入参对象中
  3. 调用Validator组件对已经绑定好的请求消息的入参进行数据合法性校验,并最终生成数据绑定结果BindingResult对象
  4. Spring MVC抽取BindingResult中的入参对象和检验错误对象,将他们赋给处理方法的响应入参
  5. Spring MVC通过反射机制对目标方法进行解析,将请求消息绑定到处理方法的入参中。

数据转换

2.1 ConversionService
  • Spring MVC 上下文中内建了很多转换器,可完成大多数 Java 类型的转换工作。
  • Spring3.0 添加了一个通用的类型转换模块,位于 org.springframework.core.convert 包中
  • ConversionService 接口是类型转换的核心接口
需转换的类将以成员变量的方式出现在宿主类中,TypeDescriptor 不但 描述了需转换类的信息,还描述了从宿主类的上下文信息,如成员变量 上的注解,成员是否是数组、集合或 Map 的方式呈现等
Modifier and Type Method and Description
boolean canConvert(Class> sourceType, Class> targetType) 判断是否可以将一个 java 类转换为另一个 java 类
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType)
T convert(Object source, Class targetType) 将原类型对象转换为目标类型对象.
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor t argetType) 将对象从原类型对象转换为目标类型对象,此时往往会用到所在宿主类 的上下文信息
2.2 自定义类型转换器

Spring 在 org.springframework.core.convert.converter 包中定义了 3 种类型转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionServiceFactroyBean 中:

  • Converter<S,T>:将S类型对象转换为T类型对象
  • ConverterFactory:将相同系列多个Converter封装在一起.如果希望将一种类
    型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及
    Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
  • GenericConverter:会根据源类对象及目标类对象所在的宿主类找那个的上下文信息进行类型转换

ConverstionServiceFactoryBean 的 converters 属性可以接受 Converter、ConverterFactory、GenericConverter 或 ConditionalGenericConverter 接口的实现类,并把这些转换器的转换逻辑统一封装到一个 ConverstionService 实例对象中(GenericConversionService),Spring 在 Bean属性配置及 Spring MVC 请求消息绑定时将利用这个 ConversionService 实例完成类型转换工作。

实际应用中常用的是Converter<S,T>,下面通过他实现一个自定义的类型转换器:
关键步骤:

  1. 实现Converter接口,他有两个泛型,S:是转换前的了类型,T:是转换后的类型 ,实现Converter接口的conver方法,在方法中定制对S类型如何转换换成T类型的规则
  2. 在springmvc配置文档中将自定义的Converter配置在ConversionService中
  3. 告诉SpringMVC使用我们自定义的类型转换器

假设处理方法有一个 User 类型的入参,我们希望将一个格式化的请求字符串直接转为 User对象,该字符串格式如(小明:男:软件工程:软工3306班:1134556)

  • 编写自定义类型转换器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.xzy.converter;

    import com.xzy.bean.Student;
    import org.springframework.core.convert.converter.Converter;

    public class implements Converter<String, Student> {



    public Student convert(String param) {
    Student student = new Student();

    if (null != param && !"".equals(param)) {
    String[] pa=param.split(":");
    student.setName(pa[0]);
    student.setGender(pa[1]);
    student.setSclass(pa[2]);
    student.setMajor(pa[3]);
    student.setSid(pa[4]);
    }
    return student;
    }
    }
  • 在SpringMVC配置文档中将自定义的Converter方在IOC容器中交给Spring管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!--该 标 签 会 创 建 并 注 册 一 个 默 认 的 DefaultAnnotationHandlerMapping 和一个ReqeustMappingHandlerAdpter实现,除此之外<mvc:annotaion-driven/>标签还会注册一个默认的ConversionService(FormattingConversionServiceFactoryBean)以满足大多数类型转换 的需求 ,当用到自定义类型转换器时,需要用
    <mvc:annotation-driven conversion-service=”xxx”/>覆盖默认-->

    <!--配置自定义的类型转换器-->
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
    <set>
    <bean class="com.xzy.converter.StringToStudentConverter"/>
    </set>
    </property>
    </bean>

<mvc:annotation-driven conversion-service=”xxx”/>覆盖默认的类型转换器

1
2
<!--在<mvc:annotation-driver中配置conversion-service覆盖默认的转换器>-->
<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>

控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.xzy.contorller;

import com.xzy.bean.Student;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class StudentController {

@RequestMapping("/addStudent")
public String addStudent(@RequestParam("stuInfo") Student student, Model model) {
System.out.println("封装的" + student);
model.addAttribute("stuinfo",student);
return "success";
}
}

测试结果:

数据格式化

Spring 使用转换器进行源类型对象到目标类型对象的转换,Spring 的转换不提供输入及
输出信息格式化工作,像日期、时间、数字、货币等数据都具有一定格式的,在不同的本地化环境中,同一类型的数还会相应地呈现不同的显示格式。
如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格
式化的数据,是 spring 格式化框架要解决的问题,Spring 引入了一个新的格式化框架,这个框架位于 org.springframework.format 类包中,其中最重要的一个接口 Formatter
Spring 的 org.springframework.format.datetime 包中提供了一个用于时间对象格式化的
DateFormatter 实现类,而 org.springframework.format.number 包中提供了 3 个用于数字
对象格式化的实现类。

  • NumberFormatter:用于数字类型对象的格式化
  • CurrencyFormatter:用于货币类型对象的格式化
  • PercentFormatter: 用于百分数数字类型对象的格式化

示例:
有一个员工类employee.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.xzy.bean;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;

import java.math.BigDecimal;
import java.util.Date;

public class employee {


private String mame;

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birth; //生日

//表示3位一个逗号,保留两位小数
@NumberFormat(pattern = "#,###.##")
private Double salary; //薪水


//省略getter、setter

@Override
public String toString() {
return "employee{" +
"mame='" + mame + ''' +
", birth=" + birth +
", salary=" + salary +
'}';
}
}

要使注解可以发挥作用还需要在注解中配置如下信息:

1
2
3
4
5
6
7
8
<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.xzy.converter.StringToStudentConverter"/>
</set>
</property>
</bean>

    对属性对象的输入/输出进行格式化,从其本质上讲依然属于 “类型转换” 的范畴。Spring 在格式化模块中定义了一个实现 ConversionService 接口的FormattingConversionService 实现类,该实现类扩展了GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能。
    FormattingConversionService 拥有FormattingConversionServiceFactroyBean 工厂类,后者用于在 Spring 上下文中构造前者FormattingConversionServiceFactroyBean 内部已经注册了 :
NumberFormatAnnotationFormatterFactroy:
支持对数字类型的属性使用 @NumberFormat 注解JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用 @DateTimeFormat 注解
装配了 FormattingConversionServiceFactroyBean 后,就可 以在 Spring MVC 入
参绑定及模型数据输出时使用注解驱动了。
<mvc:annotation-driven/>默认创建的ConversionService 实例即为 FormattingConversionServiceFactroyBean.

数据校验

应用进程在执行业务逻辑前,必须通过数据校验保证接收到的输入数据是正确合法的,如代表生日的日期应该是一个过去的时间、工资的数值必须是一个整数等。一般情况下,应用进程的开发时分层的,不同层的代码由不同的开发人员负责。很多时候,同样的数据验证会出现在不同的层中,这样就会导致代码冗余,违反了DRY原则。为了避免这样的情况,最好将验证逻辑和响应的域模型进行绑定,将代码验证的逻辑集中起来管理。

JSR-303

JSR-303是Java为Bean数据合法校验锁提供的标准框架,它已经包含在JavaEE 6.0中。JSR-303通过在Bean属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。可以通过http://jcp.org/en/jsr/detail?id=303了解更多详细内容。
JSR-303定义了一套可标注在成员变量、属性方法上的校验注解:

JSR-303 支持 XML 风格的和注解风格的验证,接下来我们首先看一下如何和 Spring 集成。 1. 导入jar包,此处使用 Hibernate-validator 实现(版本:hibernate-validator-6.0.17.Final-dist.zip),他的Maven依赖如下:
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
  1. 在Spring配置中添加JSR-303验证框架支持

    1
    2
    3
    4
    <!--配置对JSR-303的支持-->
    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    </bean>
  2. 通过 ConfigurableWebBindingInitializer 注册 validator

    1
    2
    3
    4
    5
    <!--注册validator-->
    <bean id="webBinding" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
    <property name="conversionService" ref="conversionService"/>
    <property name="validator" ref="validator"/>
    </bean>
  3. 使用 JSR-303 验证框架注解为模型对象指定验证信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
      package com.xzy.bean;


    import javax.validation.constraints.*;

    public class Student {

    @NotEmpty
    private String name;

    @Size(min = 7,max=10)
    private String sid; //学号


    @Pattern(regexp = "/^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/")
    private String phone; //手机号码

    @NotBlank
    private String sclass;

    @NotEmpty
    private String major;

    //省略getter、setter

    @Override
    public String toString() {
    return "Student{" +
    "姓名:'" + name + ''' +
    ", 学号:'" + sid + ''' +
    ", 手机:'" +phone + ''' +
    ", 班级:'" + sclass + ''' +
    ", 专业:'" + major + ''' +
    '}';
    }
    }
  4. 控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    //@Vaild就是告诉SpringMVC 把数据绑定好后要根据Bean里面的校验规则校验
    @RequestMapping(value = "/addStudent", method = RequestMethod.POST)
    public String addStudent2(
    @Valid @ModelAttribute Student student,
    Errors error,
    Model model) {
    logger.info(student);
    model.addAttribute("student", student);
    if (error.hasErrors()) {
    System.out.println(error);
    logger.error(error);
    return "add"; //如果有错误,就返回填写页面重新填写
    }
    return "success";
    }

    @RequestMapping(value="/{formName}")
    public String loginForm(
    @PathVariable String formName,
    Model model){
    System.out.println(formName);
    Student student= new Student();
    model.addAttribute("student",student);
    // 动态跳转页面

    return formName;
    }

通过在命令对象上注解@Valid 来告诉 Spring MVC 此命令对象在绑定完毕后需要进行 JSR-303验证,如果验证失败会将错误信息添加到 Errors 错误对象中。

  1. 验证失败后回到填写表单的页面(/WEB-INF/jsp/pages/add.jsp)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <form:form modelAttribute="student" method="post" action="addStudent.htm">
    <table>
    <tr>
    <td>姓名:</td>
    <td><form:input path="name"/></td>
    <td><form:errors path="name" cssStyle="color:red"/></td>
    </tr>
    <tr>
    <td>学号:</td>
    <td><form:input path="sid"/></td>
    <td><form:errors path="sid" cssStyle="color:red"/></td>
    </tr>
    <tr>
    <td>手机:</td>
    <td><form:input path="phone"/></td>
    <td><form:errors path="phone" cssStyle="color:red"/></td>
    </tr>
    <tr>
    <td>班级:</td>
    <td><form:input path="sclass"/></td>
    <td><form:errors path="sclass" cssStyle="color:red"/></td>
    </tr>
    <tr>
    <td>专业:</td>
    <td><form:input path="major"/></td>
    <td><form:errors path="major" cssStyle="color:red"/></td>
    </tr>
    <tr>
    <td><input type="submit" value="提交"/></td>
    </tr>
    </table>
    </form:form>
    </body>
    </html>

测试结果:

自定义国际化错误消息提示

在上面的进程中有一个不好的地方,错误消息不是 我们自定义的,而且都是英文的,下面我们来看看如何在通过国际化配置文档实现自定义国际化错误消息提示。
使用 System.out.println("错误码:"+fieldError.getCodes());可以得到错误的错误码,每种错误都定义了4中错误码,如下:

他们从上到下所包含的范围由小到大,我们在写国际化配置文档的时候,每条配置的key必须是4个code中的一个code。

error_en_US.properties

1
2
3
4
5
NotBlank.student.name=name must not be empty
Size.student.sid=the length must between {2} and {1}
Pattern.student.phone=please write a right phone number
NotBlank.student.sclass=class must not be empty
NotEmpty.student.major=major must not be empty

error_zh_CN.properties

1
2
3
4
5
NotBlank.student.name=姓名不能为空!
Size.student.sid=长度应该在{2}和{1}之间!
Pattern.student.phone=请填写正确的手机号码!
NotBlank.student.sclass=班级不能为空!
NotEmpty.student.major=专业不能为空!

在springmvc.xml文档中配置国际化资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--配置国际化资源-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames" value="error"/>
<!-- <property name="basenames" value="message"/>-->
<property name="useCodeAsDefaultMessage" value="false"/>
<property name="cacheSeconds" value="0"/>
<!--配置字符编码为UTF-8:注意properties的编码格式也应该是UTF-8的,否者即使你设置了字符编码过滤器也会乱码-->
<property name="defaultEncoding" value="UTF-8"/>
</bean>

<!-- 主要用于获取请求中的locale信息,将其转为Locale对像,获取LocaleResolver对象-->
<mvc:interceptors>
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
</mvc:interceptors>


<!-- 配置SessionLocaleResolver用于将Locale对象存储于Session中供后续使用 -->
<bean id="SessionLocaleResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>

测试结果: