1、SpringMVC数据绑定流程分析
假如表单中新加入一个字段Birth
Birth: <form:input path="birth"/>
在实体类Employee中
private Date birth;
那么我们需要注意:
-
表单输入的是一个String类型,而目标实体类的字段是Date类型,涉及到数据类型转换
-
表单中不是什么格式日期都可以输入,要指定格式,涉及到数据类型格式化
-
表单中输入的生日日期是一个过期的日期,而不是将来或现在,涉及到数据校验
=========================================================================
接下来探讨以下SpringMVC的数据绑流程 -
Spring MVC 主框架将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder (数据绑定器)实例对象
-
DataBinder 调用装配在 Spring MVC 上下文中的ConversionService 组件进行数据类型转换、数据格式化工作。将 Servlet 中的请求信息填充到入参对象中
-
调用 Validator 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData 对象
-
如果在数据类型转换、数据格式化、数据校验的过程中出现错误,Spring MVC将会把错误放到BindingResult中,并抽取 BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
Spring MVC 通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder,运行机制如下:
2、自定义类型转换器(了解)
SpringMVC内置了很多类型转换器,可以完成大多数Java类型的转换工作,本节内容仅供了解即可
- ConversionService 是 Spring 类型转换体系的核心接口
- 可以利用 ConversionServiceFactoryBean 在 Spring 的 IOC容器中定义一个 ConversionService. Spring 将自动识别出IOC 容器中的 ConversionService,并在 Bean 属性配置及Spring MVC 处理方法入参绑定等场合使用它进行数据的转换
- 可通过 ConversionServiceFactoryBean 的 converters 属性注册自定义的类型转换器
2.1 Spring支持的转换器
Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到ConversionServiceFactroyBean 中:
- Converter<S,T>:将 S 类型对象转为 T 类型对象
- ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
- GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
2.2 自定义转换器示例
<mvc:annotation-driven conversion-service=“conversionService”/>
会将自定义的 ConversionService 注册到Spring MVC 的上下文中
在测试页面中加上以下代码,目标是把一个employee字符串转换为一个对象
<form action="testConversionServiceConverer" method="POST">
<!-- 目标是把一个employee字符串转换为一个对象,字符串格式是lastname-email-gender-department.id 例如: GG-gg@111.com-0-105 -->
Employee: <input type="text" name="employee"/>
<input type="submit" value="Submit"/>
</form>
目标方法
@Autowired
private EmployeeDao employeeDao;
@RequestMapping("/testConversionServiceConverer")
public String testConverter(@RequestParam("employee") Employee employee){
System.out.println("save: " + employee);
employeeDao.save(employee);
return "redirect:/emps";
}
自定义转换器,需要实现Converter接口(org.springframework.core.convert.converter.Converter)
package com.springmvc.converters;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import com.springmvc.crud.entities.Department;
import com.springmvc.crud.entities.Employee;
@Component
public class EmployeeConverter implements Converter<String, Employee> {
@Override
public Employee convert(String source) {
if(source != null){
String [] vals = source.split("-");
//GG-gg@atguigu.com-0-105
if(vals != null && vals.length == 4){
String lastName = vals[0];
String email = vals[1];
Integer gender = Integer.parseInt(vals[2]);
Department department = new Department();
department.setId(Integer.parseInt(vals[3]));
Employee employee = new Employee(null, lastName, email, gender, department);
System.out.println(source + "--convert--" + employee);
return employee;
}
}
return null;
}
}
SpringMVC配置文件配置
<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>
<!-- 配置 ConversionService -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<ref bean="employeeConverter"/>
</set>
</property>
</bean>
3、关于mvc:annotation-driven
<mvc:annotation-driven/>
会自动注册RequestMappingHandlerMapping、RequestMappingHandlerAdapter 与ExceptionHandlerExceptionResolver 三个bean。
而且还提供以下支持:
- 支持使用 ConversionService 实例对表单参数进行类型转换
- 支持使用 @NumberFormat annotation、@DateTimeFormat注解完成数据类型的格式化
- 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
- 支持使用 @RequestBody 和 @ResponseBody 注解
既没有配置 <mvc:default-servlet-handler/>
也没有配置 <mvc:annotation-driven/>
配置了 <mvc:default-servlet-handler/>
但没有配置 <mvc:annotation-driven/>
既配置了 <mvc:default-servlet-handler/>
又配置 <mvc:annotation-driven/>
建议: 开发的时候最好加上<mvc:annotation-driven/>
4、关于注解@InitBinder
- @InitBinder 标识的方法,可以对 WebDataBinder 对象进行初始化。WebDataBinder 是 DataBinder 的子类,用于完成由表单字段到 JavaBean 属性的绑定
- @InitBinder方法不能有返回值,它必须声明为void
- @InitBinder方法的参数通常是是 WebDataBinder
目标方法:
@InitBinder
public void initBinder(WebDataBinder binder){
binder.setDisallowedFields("lastName");
}
WebDataBinder有很多方法,以上setDisallowedFields方法是不对lastName字段进行赋值。
使用场景:假如现在我们需要对一个User的角色选项进行赋值,这个角色是个多选框,当他是一个多选的时候,里面放的是角色的一个一个ID,而我们User对象里面可能是一个集合,这个时候是没有办法把ID映射成一个集合的,这个就需要@InitBinder注解来手动进行映射
5、数据格式化
- 对属性对象的输入/输出进行格式化,从其本质上讲依然属于 “类型转换” 的范畴。(所以说数据类型的转换和数据格式化一般都是一起发生的)
- Spring 在格式化模块中定义了一个实现ConversionService 接口的FormattingConversionService 实现类,该实现类扩展了GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能
- FormattingConversionService 拥有一个FormattingConversionServiceFactroyBean 工厂类,后者用于在 Spring 上下文中构造前者
- FormattingConversionServiceFactroyBean 内部已经注册了:NumberFormatAnnotationFormatterFactroy(支持对数字类型的属性使用 @NumberFormat 注解)、JodaDateTimeFormatAnnotationFormatterFactroy(支持对日期类型的属性使用 @DateTimeFormat 注解)
- 装配了 FormattingConversionServiceFactroyBean 后,就可以在 Spring MVC 入参绑定及模型数据输出时使用注解驱动了,
<mvc:annotation-driven/>
默认创建的ConversionService 实例即为FormattingConversionServiceFactroyBean
5.1 日期格式化
在文章的最上面我们知道,假如表单中新加入一个字段Birth
Birth: <form:input path="birth"/>
在实体类Employee中
private Date birth;
这样表单中的birth和实体类中的birth类型是不一样的,这时候我们需要进行数据格式化
注意:实体类中一定要有birth属性,不然表单是不能够正常显示的
================================================================
这个时候在表单中填写日期1999-02-02,提交后会报400错误
SpringMVC在进行类型转换的时候要知道我们把怎么样的一个字符串转换为Date类型
解决方法:
- 在SpringMVC的配置文件中要配置
<mvc:annotation-driven/>
- 在Employee实体类中的birth属性中加上以下注解:
//告诉SpringMVC Date的样式是yyyy-MM-dd
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
5.2 数值格式化
与日期格式化类似,也要在SpringMVC的配置文件中要配置<mvc:annotation-driven/>
表单中新加入一个字段salary
Salary: <form:input path="salary"/>
在实体类Employee中
@NumberFormat(pattern="#,###,###.#")
private Float salary
输入的字符串按照特定的格式可以为:1,234,567.8
================================================================
这里注意一个小问题:前面我们已经知道DateBinder将错误结果存入BindingResult,如果数据格式化发生错误,我们可以在BindingResult取出错误信息
@RequestMapping(value="/emp", method=RequestMethod.POST)
public String save(Employee employee, BindingResult result){//从BindingResult中取出错误信息
System.out.println("save: " + employee);
if(result.getErrorCount() > 0){
System.out.println("出错了!");
for(FieldError error:result.getFieldErrors()){
System.out.println(error.getField() + ":" + error.getDefaultMessage());
}
}
employeeDao.save(employee);
return "redirect:/emps";
}
6、数据校验
验证标准:JSR303
- JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中
- JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对 Bean进行验证
JSR303是一个标准,所以需要有一个实现产品作为支持
实现产品:Hibernate Validator
- Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
如何校验呢? - Spring 本身并没有提供 JSR303 的实现,所以必须将JSR303 的实现者的 jar 包放到类路径下。
- Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中。
<mvc:annotation-driven/>
会默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参上标注 @valid 注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作
6.1 如何校验
第一步:加入 hibernate validator 验证框架的 jar 包
第二步:
第三步:在 SpringMVC 配置文件中添加 <mvc:annotation-driven/>
第四步:需要在 bean 的属性上添加对应的注解
private Integer id;
@NotEmpty //不能为空
private String lastName;
@Email //要为Email标准格式
private String email;
//1 male, 0 female
private Integer gender;
private Department department;
@Past //要为过去的时间
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
@NumberFormat(pattern="#,###,###.#")
private Float salary;
第五步:在目标方法 bean 类型的前面添加 @Valid 注解
@RequestMapping(value="/emp", method=RequestMethod.POST)
public String save(@Valid Employee employee, BingingResult result){
System.out.println("save: " + employee);
if(result.getErrorCount() > 0){
System.out.println("出错了!");
for(FieldError error:result.getFieldErrors()){
System.out.println(error.getField() + ":" + rror.getDefaultMessage());
}
}
employeeDao.save(employee);
return "redirect:/emps";
}
这个时候执行可能会报错:
解决方法一:换一个外置的Tomcat,可能可以解决
解决方法二:找到外置tomcat目录下的Lib,强制删除el-api.jar,并用javax-el-x.y.z.jar, el-api-x.y.jar,javax-el-api-x.y.z.jar复制到lib目录下
6.2 出错页面跳转
@RequestMapping(value="/emp", method=RequestMethod.POST)
public String save(@Valid Employee employee, BingingResult result,
Map<String, Object> map){
System.out.println("save: " + employee);
if(result.getErrorCount() > 0){
System.out.println("出错了!");
for(FieldError error:result.getFieldErrors()){
System.out.println(error.getField() + ":" + rror.getDefaultMessage());
}
//若验证出错, 则转向定制的页面
map.put("departments", departmentDao.getDepartments());//用于表单回显
return "input";
}
employeeDao.save(employee);
return "redirect:/emps";
}
注意:
- Spring MVC 是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是 BindingResult 或Errors 类型(BindingResult 扩展了 Errors 接口),这两个类都位于org.springframework.validation 包中
- 需校验的Bean 对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他的入参
6.3 如何在页面上显示错误消息
在表单页面上写下以下代码
<!-- 显示所有错误消息 -->
<form:errors path="*"></form:errors>
<!-- 显示Email的错误消息 -->
Email: <form:input path="email"/>
<form:errors path="email"></form:errors>
6.4 定制错误消息
第一步:创建一个国际化的资源文件(项目src文件路径下)
用于定制错误消息的国际化资源文件编写规则:
- 当一个属性校验失败后,校验框架会为该属性生成 4 个消息代码,这些代码以校验注解类名为前缀,结合modleAttribute、属性名及属性类型名生成多个对应的消息代码:例如 User 类中的 password 属性标注了一个 @Pattern 注解,email属性标注一个@Email注解,那么国际化资源文件应该配置为:
Pattern.user.password = 密码不合法
Email.user.email = 邮箱格式不合法
第二步:在SpringMVC配置文件中配置国际化资源文件
<!-- 配置国际化资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
若数据类型转换或数据格式转换时发生错误,或该有的参数不存在,或调用处理方法时发生错误,都会在隐含模型中创建错误消息。其错误代码前缀说明如下:
- required:必要的参数不存在。如 @RequiredParam(“param1”) 标注了一个入参,但是该参数不存在
- typeMismatch:在数据绑定时,发生数据类型不匹配的问题
- methodInvocation:Spring MVC 在调用处理方法时发生了错误
以typeMismatch举一个例子,在国际化资源文件中配置:
typeMismatch.user.birth = Birth不是一个合法日期.