数据绑定
数据绑定流程
- SpringMVC主框架将ServletRequest对象及目标方法的入参实例传递给
WebDataBinderFactory
实例,以创建DataBinder
实例对象。 -
DataBinder
调用装配在SpringMVC上下文中的ConversionService
组件进行数据类型转换、数据格式化工作,将Servlet中的请求信息填充到入参对象中。 - 调用
Validator
组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData
对象。 - SpringMVC抽取
BindingResult
中的入参对象和校验错误对象,将他们赋给处理方法的响应入参。
类型转换
自定义类型转换器
ConversionService
是Spring类型转换体系的核心接口,可以利用ConversionServiceFactoryBean
在Spring的IOC容器中定义一个ConversionService。Spring将自动识别出IOC容器中的ConversionService
,并在bean属性配置及SpringMVC处理方法入参绑定等场合使用它进行数据的转换。
可以通过ConversionServiceFactoryBean
的converters
属性注册自定义的类型转换器。
Spring支持的转换器
Spring定义了3种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到ConversionServiceFactroyBean
中:
-
Converter
:将S类型对象转为T类型对象。 -
ConverterFactory
:将相同系列多个“同质”Converter封装在一起,如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将String转换为Number及Number子类(Integer、Long、Double等)对象)可使用该转换器工厂类。 -
GenericConverter
:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换。
在配置文件中使用
会将自定义的ConversionService注册到SpringMVC的上下文中。,其中xxxxx是自定义的类型转换器名,例如:
我们来自定义一个类型转换器,我们希望输入一个字符串(lastname-email-gender-department.id),然后提交后能转换为一个Employee对象。
- 首先建立我们的两个实体类及对应的dao类
package com.cerr.springmvc.crud.entities;
public class Employee {
private Integer id;
private String lastname;
private String email;
private Integer gender;
private Department department;
public Employee(){}
public Employee(Integer id, String lastname, String email, Integer gender, Department department) {
this.id = id;
this.lastname = lastname;
this.email = email;
this.gender = gender;
this.department = department;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastname='" + lastname + '\'' +
", email='" + email + '\'' +
", gender=" + gender +
", department=" + department +
'}';
}
}
package com.cerr.springmvc.crud.entities;
public class Department {
private Integer id;
private String departmentName;
public Department() {}
public Department(Integer id, String departmentName) {
super();
this.id = id;
this.departmentName = departmentName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDepartmentName() {
return departmentName;
}
public void setDepartmentName(String departmentName) {
this.departmentName = departmentName;
}
}
package com.cerr.springmvc.crud.dao;
import com.cerr.springmvc.crud.entities.Department;
import com.cerr.springmvc.crud.entities.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Repository
public class EmployeeDao {
private static Map employees = null;
@Autowired
private DepartmentDao departmentDao;
static{
employees = new HashMap ();
employees.put(1001, new Employee(1001, "E-AA", "[email protected]", 1, new Department(101, "D-AA")));
employees.put(1002, new Employee(1002, "E-BB", "[email protected]", 1, new Department(102, "D-BB")));
employees.put(1003, new Employee(1003, "E-CC", "[email protected]", 0, new Department(103, "D-CC")));
employees.put(1004, new Employee(1004, "E-DD", "[email protected]", 0, new Department(104, "D-DD")));
employees.put(1005, new Employee(1005, "E-EE", "[email protected]", 1, new Department(105, "D-EE")));
}
private static Integer initId = 1006;
public void save(Employee employee){
if(employee.getId() == null){
employee.setId(initId++);
}
employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
employees.put(employee.getId(), employee);
}
public Collection getAll(){
return employees.values();
}
public Employee get(Integer id){
return employees.get(id);
}
public void delete(Integer id){
employees.remove(id);
}
}
- 建立我们自定义的转换器类,需要实现
Converter
接口
package com.cerr.springmvc.converters;
import com.cerr.springmvc.crud.entities.Department;
import com.cerr.springmvc.crud.entities.Employee;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
//该注释让该类能加载到IOC容器中
@Component
public class EmployeeConverter implements Converter {
/**
* 这是转换的方法,对传入的字符串进行分割。
* 然后填充Employee的参数来初始化一个Employee实例,然后返回
* @param s
* @return
*/
@Override
public Employee convert(String s) {
if (s != null){
String [] vals = s.split("-");
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(employee);
return employee;
}
}
return null;
}
}
- 在配置文件中配置:
测试:
package com.cerr.springmvc.test;
import com.cerr.springmvc.crud.dao.EmployeeDao;
import com.cerr.springmvc.crud.entities.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class SpringMVCTest1 {
@Autowired
private EmployeeDao employeeDao;
@RequestMapping(value = "/testConversionServiceConverer",method = RequestMethod.POST)
public String testConverter(@RequestParam("employee") Employee employee){
employeeDao.save(employee);
System.out.println(employee);
return "redirect:/emps";
}
}
表单:
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%--
Created by IntelliJ IDEA.
User: 白菜
Date: 2019/11/13
Time: 21:10
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
Title
输入:[email protected]
结果:在控制台成功打印出Employee(转换成功)
关于mvc:annotation-driven
会自动注册RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
与ExceptionHandlerExceptionResolver
三个bean。
还将提供以下支持:
- 支持使用
ConversionService
实例对表单参数进行类型转换 - 支持使用
@NumberFormatannotation
、@DateTimeFormat
注解完成数据类型的格式化 - 支持使用
@Valid
注解对JavaBean实例进行JSR 303验证 - 支持使用
@RequestBody
和@ResponseBody
注解
@InitBinder
由@InitBinder
标识的方法,可以对WebDataBinder对象进行初始化。WebDataBinder是DataBinder的子类,用于完成由表单字段到JavaBean属性的绑定。
@InitBinder
方法不能有返回值,必须声明为void;@InitBinder
方法的参数通常是WebDataBinder。
例如我们这里定义一个initBinder方法,然后方法体里面的设置对于表单的lastname属性不允许绑定到JavaBean顺序。
@InitBinder
public void initBinder(WebDataBinder binder){
binder.setDisallowedFields("lastname");
}
数据格式化
对属性对象的输入/输出进行格式化,从其本质上讲依然属于“类型转换”的范畴。Spring在格式化模块中定义了一个实现ConversionService
接口的FarmattingConversionService
实现类,该实现类扩展了GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能。FormattingConversionService
拥有一个FormattingConversionServiceFactoryBean
工厂类,后者用于在Spring上下文中构造前者。
对于FormattingConversionServiceFactoryBean
,内部已经注册了:
-
NumberFormatAnnotationFormatterFactory
:支持对数字类型属性使用@NumberFormat
注解 -
JodaDateTimeFormatAnnotationFormatterFactory
:支持对日期类型的属性使用@DataTimeFormat
注解
如果装配了FormattingConversionServiceFactoryBean
,就可以在SpringMVC入参绑定及模型数据输出时使用注解驱动了,
默认创建的ConversionService
实例即为FormattingConversionServiceFactoryBean
。
日期格式化
@DateTimeFOrmat
注解可对java.util.Date
、java.util.Calendar
、java.long.Long
时间类型进行标注,比较重要的属性:
-
pattern
属性:类型为字符串,指定解析/格式化字段数据的模式,如:"yyyy-MM-dd hh:mm:ss"。
package com.cerr.springmvc.crud.entities;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import java.util.Date;
public class Employee {
private Integer id;
private String lastname;
private String email;
private Integer gender;
private Department department;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birth;
private Float sarly;
public Employee(){}
public Employee(Integer id, String lastname, String email, Integer gender, Department department) {
this.id = id;
this.lastname = lastname;
this.email = email;
this.gender = gender;
this.department = department;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Float getSarly() {
return sarly;
}
public void setSarly(Float sarly) {
this.sarly = sarly;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastname='" + lastname + '\'' +
", email='" + email + '\'' +
", gender=" + gender +
", department=" + department +
", birth=" + birth +
", sarly=" + sarly +
'}';
}
}
在上面的例子中,我们注解的pattern
为"yyyy-MM-dd",因此当我们输入的日期为"yyyy-MM-dd"时都能给解析并转换成Date
。
数值格式化
@NumberFormat
可对类似数字类型的属性进行标注,它拥有两个互斥的属性:
-
style
类型为Numberformat.Style
。用于指定样式类型,包括三种:Style.NUMBER
(正常数字类型)、Style.CURRENCY
(货币类型)、Style.PERCENT
(百分数类型)。 -
pattern
类型为String,自定义样式,如pattern="#,###"
。
package com.cerr.springmvc.crud.entities;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import java.util.Date;
public class Employee {
private Integer id;
private String lastname;
private String email;
private Integer gender;
private Department department;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birth;
@NumberFormat(pattern = "#,###,###.#")
private Float sarly;
public Employee(){}
public Employee(Integer id, String lastname, String email, Integer gender, Department department) {
this.id = id;
this.lastname = lastname;
this.email = email;
this.gender = gender;
this.department = department;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Float getSarly() {
return sarly;
}
public void setSarly(Float sarly) {
this.sarly = sarly;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastname='" + lastname + '\'' +
", email='" + email + '\'' +
", gender=" + gender +
", department=" + department +
", birth=" + birth +
", sarly=" + sarly +
'}';
}
}
上面的代码标识了@NumberFormat(pattern = "#,###,###.#")
后,例如输入1,234,567.8,就能自动转成数字。
数据校验
JSR303
JSR 303是Java为Bean数据合法性校验提供的标准框架,它已经包含在JavaEE6.0中,JSR 303通过在Bean属性上标注类似于@NotNull
、@Max
等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
注解 | 功能说明 |
---|---|
@Null |
被注释的元素必须为null |
@NotNull |
被注释的元素必须不为null |
@AssertTure |
被注释的元素必须为true |
@AssertFalse |
被注释的元素必须为false |
@Min(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) |
被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) |
被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max,min) |
被注释的元素必须在指定的范围内 |
@Digits(integer,fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past |
被注释的元素必须是一个过去的日期 |
@Future |
被注释的元素必须是一个将来的日期 |
@Pattern(value) |
被注释的元素必须符合指定的正则表达式 |
Hibernate Validator扩展注解
Hibernate Validator是JSR 303的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
注解 | 功能说明 |
---|---|
@Email |
被注释的元素必须是电子邮箱地址 |
@Length |
被注释的字符串的大小必须在指定的范围内 |
@NotEmpty |
被注释的字符串必须非空 |
@Range |
被注释的元素必须在合适的范围内 |
SpringMVC数据校验
Spring4.0拥有自己独立的数据校验框架,同时支持JSR 303标准的校验框架。Spring在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC中,可直接通过注解驱动的方式进行数据校验。
Spring的LocalValidatorFactoryBean
既实现了Spring的Validator
接口,也实现了JSR 303的Validator
接口,只要在Spring容器中定义一个LocalValidatorFactoryBean
,即可将其注入到需要数据校验的Bean中。
Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的jar包放到类路径下。
会默认装配好一个LocalValidatorFactroyBean
,通过在处理方法的入参上标注@valid
注解即可让SpringMVC在完成数据绑定后执行数据校验的工作。
在已经标注了JSR 303注解的表单/命令对象前标注一个@Valid
,SpringMVC框架在将请求参数绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验。
SpringMVC是通过对处理方法签约的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是BindingResult
或Errors
类型,这两个类都位于org.springframework.validation
包中。要注意的一点是:需要校验的Bean对象和其绑定结果对象或错误对象是成对出现的,它们之间不允许声明其他的入参
例如下面的代码是错误的:
@RequestMapping(value = "/emp",method = RequestMethod.POST)
public String save(@Valid Employee employee, Map map,BindingResult result){
return "redirect:/emps";
}
Employee和BindingResult两个参数应该是在一起的,例如下面的才是正确的:
@RequestMapping(value = "/emp",method = RequestMethod.POST)
public String save(@Valid Employee employee, BindingResult result,Map map){
return "redirect:/emps";
}
Errors
接口提供了获取错误信息的方法,如getErrorCount()
或getFieldErrors(String field)
,BindingResult
扩展了Errors
接口。
@RequestMapping(value = "/emp",method = RequestMethod.POST)
public String save(@Valid Employee employee, BindingResult result,Map map){
System.out.println("save:"+employee);
/**
* 如果转换出错
*/
if (result.getErrorCount() > 0){
System.out.println("出错了:");
for (FieldError error : result.getFieldErrors()){
System.out.println(error.getField() + ":" + error.getDefaultMessage());
}
map.put("departments",departmentDao.getDepartments());
//若验证出错,则转向定制的页面
return "input";
}
employeeDao.save(employee);
return "redirect:/emps";
}
SpringMVC整合扩展的Hibernate Validator验证框架数据校验的步骤
- 使用JSR 303验证标准完成数据校验。
- 加入hibernate validator验证框架的jar包。
- 在SpringMVC配置文件中添加
- 需要在bean的属性上添加对应的注解
- 在目标方法bean类型的前面添加
@Valid
注解
首先在idea中加入jar包,并添加在Artifacts中
然后在配置文件中添加
,因为该配置在上面类型转换时定义了一个自定义的类型转换器,因此配置了FormattingConversionServiceFactoryBean
,如果不需要使用自定义类型转换器的话,这部分代码可以去掉:
对上面的Employee类,我们可以做一些验证如下,添加@NotNull
、@Email
、@Past
注解:
package com.cerr.springmvc.crud.entities;
import org.hibernate.validator.constraints.Email;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import java.util.Date;
public class Employee {
private Integer id;
@NotNull
private String lastname;
@Email
private String email;
private Integer gender;
private Department department;
@Past
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birth;
@NumberFormat(pattern = "#,###,###.#")
private Float sarly;
public Employee(){}
public Employee(Integer id, String lastname, String email, Integer gender, Department department) {
this.id = id;
this.lastname = lastname;
this.email = email;
this.gender = gender;
this.department = department;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Float getSarly() {
return sarly;
}
public void setSarly(Float sarly) {
this.sarly = sarly;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", lastname='" + lastname + '\'' +
", email='" + email + '\'' +
", gender=" + gender +
", department=" + department +
", birth=" + birth +
", sarly=" + sarly +
'}';
}
}
并在处理的方法中的入参中对要校验的实体类标注@Valid
,如果需要使用到BindingResult
等类的话要注意要和@Valid
标注的实体类成对存在:
@RequestMapping(value = "/emp",method = RequestMethod.POST)
public String save(@Valid Employee employee, BindingResult result,Map map){
System.out.println("save:"+employee);
/**
* 如果转换出错
*/
if (result.getErrorCount() > 0){
System.out.println("出错了:");
for (FieldError error : result.getFieldErrors()){
System.out.println(error.getField() + ":" + error.getDefaultMessage());
}
map.put("departments",departmentDao.getDepartments());
//若验证出错,则转向定制的页面
return "input";
}
employeeDao.save(employee);
return "redirect:/emps";
}
如果输入错误,则输出结果如下:
在页面上显示错误
在JSP页面上通过
显示错误消息
如果要一次性显示所有错误消息,可以将path
写为*,即:
提示消息的国际化
每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的FieldError
对象。当一个属性校验失败后,校验框架会为该属性生成4个消息代码,这些代码以校验注解类名为前缀,结合modleAttribute
、属性名及属性类型名生成多个对应的消息代码。
例如User类中的password属性标注了一个@Pattern
注解,当该属性值不满足@Pattern
所定义的规则时,就会产生以下4个错误代码:
- Pattern.user.password
- Pattern.password
- Pattern.java.lang.String
- Pattern
当使用SpringMVC标签显示错误消息时,SpringMVC会查看WEB上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息。
定制提示消息的话,我们要先定义一个国际化资源文件,例如i18n.properties文件,并且写入需要定制化的消息:
NotNull.employee.lastname=lastname\u4e0d\u80fd\u4e3a\u7a7a
Email.employee.email=Email\u4e0d\u5408\u6cd5
Past.employee.birth=Birth\u4e0d\u80fd\u662f\u4e00\u4e2a\u5c06\u6765\u7684\u65f6\u95f4