- 数据绑定,即将
ServletRequest
对象与目标方法的入参,通过DataBinder
进行绑定。实现数据类型转换、数据格式化工作。
数据绑定流程原理
- Spring MVC 主框架将
ServletRequest
对象及目标方法的入参实例传递给WebDataBinderFactory
实例,以创建DataBinder
实例对象 DataBinder
调用装配在 Spring MVC 上下文中的ConversionService
组件进行数据类型转换、数据格式化工作。将 Servlet 中的请求信息填充到入参对象中- 调用
Validator
组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData
对象 - Spring MVC 抽取
BindingResult
中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
转换器
- Spring MVC 内建了很多转换器
ConversionService converters = java.lang.Boolean->java.lang.String: org.springframework.core.convert.support.ObjectToStringConverter@f874ca java.lang.Character -> java.lang.Number : CharacterToNumberFactory@f004c9 java.lang.Character -> java.lang.String : ObjectToStringConverter@68a961 java.lang.Enum -> java.lang.String : EnumToStringConverter@12f060a java.lang.Number -> java.lang.Character : NumberToCharacterConverter@1482ac5 java.lang.Number -> java.lang.Number : NumberToNumberConverterFactory@126c6f java.lang.Number -> java.lang.String : ObjectToStringConverter@14888e8 java.lang.String -> java.lang.Boolean : StringToBooleanConverter@1ca6626 java.lang.String -> java.lang.Character : StringToCharacterConverter@1143800 java.lang.String -> java.lang.Enum : StringToEnumConverterFactory@1bba86e java.lang.String -> java.lang.Number : StringToNumberConverterFactory@18d2c12 java.lang.String -> java.util.Locale : StringToLocaleConverter@3598e1 java.lang.String -> java.util.Properties : StringToPropertiesConverter@c90828 java.lang.String -> java.util.UUID : StringToUUIDConverter@a42f23 java.util.Locale -> java.lang.String : ObjectToStringConverter@c7e20a java.util.Properties -> java.lang.String : PropertiesToStringConverter@367a7f java.util.UUID -> java.lang.String : ObjectToStringConverter@112b07f ……
- 其中,有
ConverterFactory
的实现类,内置Converter
的实现类,可通过泛型,实现多种类的转换。或者直接实现Converter
的类。(当不确认返回的类型,只知道其父类时,一般使用ConverterFactory
)@SuppressWarnings({"rawtypes", "unchecked"}) final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { @Override public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEnum(ConversionUtils.getEnumType(targetType)); // 获取枚举的class,并作为构造器的参数输入 } private static class StringToEnum<T extends Enum> implements Converter<String, T> { private final Class<T> enumType; public StringToEnum(Class<T> enumType) { this.enumType = enumType; } // 输入String,根据名字,返回枚举类型 @Override public T convert(String source) { if (source.isEmpty()) { // It's an empty enum identifier: reset the enum value to null. return null; } return (T) Enum.valueOf(this.enumType, source.trim()); } } }
final class StringToBooleanConverter implements Converter<String, Boolean> { private static final Set<String> trueValues = new HashSet<>(4); private static final Set<String> falseValues = new HashSet<>(4); static { trueValues.add("true"); trueValues.add("on"); trueValues.add("yes"); trueValues.add("1"); falseValues.add("false"); falseValues.add("off"); falseValues.add("no"); falseValues.add("0"); } @Override public Boolean convert(String source) { String value = source.trim(); if (value.isEmpty()) { return null; } value = value.toLowerCase(); if (trueValues.contains(value)) { return Boolean.TRUE; } else if (falseValues.contains(value)) { return Boolean.FALSE; } else { throw new IllegalArgumentException("Invalid boolean value '" + source + "'"); } } }
自定义转换器
ConversionService
是 Spring 类型转换体系的核心接口。- 可以利用
ConversionServiceFactoryBean
在 Spring 的 IOC 容器中定义一个ConversionService
- Spring 将自动识别出 IOC 容器中的
ConversionService
,并在 Bean 属性配置及 Spring MVC 处理方法入参绑定等场合使用它进行数据的转换 - 可通过 ConversionServiceFactoryBean 的 converters 属性注册自定义的类型转换器,例如:
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <!-- 引用类型转换器 --> <ref bean="stringToEmployee"/> </set> </property> </bean>
Spring 支持的转换器类型
- Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到
ConversionServiceFactoryBean
Converter<S,T>
:将 S 类型对象转为 T 类型对象ConverterFactory
:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类GenericConverter
:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
示例一:Converter
- Java Bean
public class Employee { private Integer id; private String name; private Gender gender; /*省略...*/
public enum Gender { male, female; }
- 提交表单。提交格式为
id-name-gender(0/1)
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Submit Employee</title> </head> <% request.setAttribute("ct", request.getContextPath()); %> <body> <form method="post" action="${requestScope.ct}/stringToEmployeeTest"> <label> Employee: id-name-gender(0/1) <input type="text" name="employee" value=""> </label><br/> <input type="submit" value="提交"><br/> </form> </body> </html>
- 请求映射。请求参数注解为
@RequestParam("employee")
,即表单提交的name。@RequestMapping("/stringToEmployeeTest") public String stringToEmployeeTest(@RequestParam("employee") Employeeemployee) { System.out.println(employee); return "success"; }
- 转换器。用于将request 的 String,转换为 Employee。通过注解
@Component
自动注册@Component public class StringToEmployeeConverter implements Converter<String, Employee> { /** * 输入格式:id-name-genderCode * * @param source * @return */ @Override public Employee convert(String source) { String[] sources = source.split("-"); return new Employee(Integer.parseInt(sources[0]), sources[1], getGenderFromCode(sources[2])); } /** * 将String转换为Gender;0代表女;1代表男; * @param code * @return */ private static Gender getGenderFromCode(String code) { if (code.equals("0")) return Gender.female; if (code.equals("1")) return Gender.male; return null; } }
- spring-mvc配置文件中,注册一个
ConversionServiceFactoryBean
,并将上述转换器放入,作为参数<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <!-- 引用类型转换器 --> <ref bean="StringToEmployeeConverter"/> </set> </property> </bean>
- 将自定义的
ConversionService
注册到 Spring MVC 的上下文中<mvc:annotation-driven conversion-service="conversionService"/>
示例二:ConverterFactory
- Employee的子类
public class Manager extends Employee { public Manager() { } public Manager(int id, String name, Gender gender) { super(id, name, gender); } }
- 转换器工厂
@Component public class StringToEmployeeConverterFactory implements ConverterFactory<String, Employee> { @Override public <T extends Employee> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEmployee<>(targetType); } private static class StringToEmployee<T extends Employee> implements Converter<String, T> { private final Class<T> targetType; public StringToEmployee(Class<T> targetType) { this.targetType = targetType; } /** * 输入格式:id-name-genderCode * * @param source * @return */ @Override public T convert(String source) { T t = null; String[] sources = source.split("-"); int id = Integer.parseInt(sources[0]); String name = sources[1]; Gender gender = "0".equals(sources[2]) ? Gender.female : Gender.male; try { t = targetType.getDeclaredConstructor().newInstance(); Class<? super T> superclass = targetType.getSuperclass(); Method setId = superclass.getDeclaredMethod("setId", Integer.class); setId.setAccessible(true); setId.invoke(t, id); Method setName = superclass.getDeclaredMethod("setName", String.class); setName.setAccessible(true); setName.invoke(t, name); Method setGender = superclass.getDeclaredMethod("setGender", Gender.class); setGender.setAccessible(true); setGender.invoke(t, gender); System.out.println(t); } catch (Exception e) { e.printStackTrace(); } return t; } } }
- 注册
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <!-- 引用类型转换器 --> <ref bean="stringToEmployeeConverterFactory"/> </set> </property> </bean>
数据格式化
- 对属性对象的输入/输出进行格式化,从其本质上讲依然属于 “类型转换” 的范畴。
- Spring 在格式化模块中定义了一个实现
ConversionService
接口的FormattingConversionService
实现类,该实现类扩展了GenericConversionService
,因此它既具有类型转换的功能,又具有格式化的功能(Spring MVC默认使用这个) FormattingConversionService
拥有一个FormattingConversionServiceFactroyBean
工厂类,后者用于在 Spring 上下文中构造前者,FormattingConversionServiceFactroyBean
内部已经注册了NumberFormatAnnotationFormatterFactroy
:支持对数字类型的属性使用@NumberFormat
注解JodaDateTimeFormatAnnotationFormatterFactroy
:支持对日期类型的属性使用@DateTimeFormat
注解
- 但是,上文使用的
ConversionServiceFactoryBean
仅包含转换器,没有包含数据格式化器。因此,需要将上文修改为<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <!-- 引用类型转换器 --> <ref bean="stringToEmployeeConverterFactory"/> </set> </property> </bean>
- 而如果没有自定义的转换器,实际上并不需要声明,只需要声明
mvc:annotation-driven
即可,因为它默认创建DefaultFormattingConversionService
。
日期格式化
@DateTimeFormat
注解可对java.util.Date
、java.util.Calendar
、java.long.Long
、java.time.LocalDate
时间类型进行标注- pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:”yyyy-MM-dd hh:mm:ss”
- iso 属性:类型为 DateTimeFormat.ISO。指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用) – 默认、ISO.DATE(yyyy-MM-dd) 、ISO.TIME(hh:mm:ss.SSSZ)、 ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
- style 属性:字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整日期/时间格式、-:忽略日期或时间格式
数值格式化
@NumberFormat
可对类似数字类型的属性进行标注,它拥有两个互斥的属性:- style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型)、Style.CURRENCY(货币类型)、 Style.PERCENT(百分数类型)
- pattern:类型为 String,自定义样式,如pattern="#,###"
示例
- Java Bean
public class Person { @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate birthday; @NumberFormat(pattern = "###,###.##") Double salary; /*省略*/ }
- 请求映射
@RequestMapping("/formatTest") public String formatTest(Person person) { System.out.println(person); return "success"; }
- 请求表单
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>format test</title> </head> <body> <form action="${pageContext.request.contextPath}/formatTest" method="post"> <label for="birthday">生日(yyyy-MM-dd):</label><input name="birthday" type="text" id="birthday"> <br/> <label for="salary">工资(###,###.##) </label><input name="salary" type="text" id="salary"><br/> <input type="submit" value="提交"><br/> </form> </body> </html>
JSR303数据校验
- JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中
- JSR 303 通过在 Bean 属性上标注类似于
@NotNull
、@Max
等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证
- Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
- Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验
- Spring 的
LocalValidatorFactroyBean
既实现了 Spring 的Validator
接口,也实现了 JSR 303 的Validator
接口。只要在 Spring 容器中定义了一个LocalValidatorFactoryBean
,即可将其注入到需要数据校验的 Bean 中。 <mvc:annotation-driven/>
会默认装配好一个LocalValidatorFactoryBean
,通过在处理方法的入参上标注@Valid
注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作- Spring 本身并没有提供 JSR303 的实现,所以必须将 JSR303 的实现者的 jar 包放到类路径下
示例
- 导包
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.2.0.Final</version> </dependency>
- Java Bean
public class People { @Email // 输入格式必须为email的格式 private String email; @NotEmpty // 必须非空 private String lastName; @Past // 必须是之前的日期 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate birthday; @Future // 必须是未来的时间 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate plan; public People(@Email String email, @NotEmpty String lastName, @Past LocalDate birthday, @Future LocalDate plan) { this.email = email; this.lastName = lastName; this.birthday = birthday; this.plan = plan; } public People() { } /*省略*/ }
- 配置文件,必须开启
<mvc:annotation-driven/>
- 请求映射
@RequestMapping("validationTest") // @Valid 表示这个参数需要被校验,bindingResult是校验的结果 public String validationTest(@Valid People people, BindingResult bindingResult) { System.out.println(people); if (bindingResult.hasErrors()) { System.out.println("发生错误"); return "forward:validation.jsp"; } return "success"; }
- 请求表单。
<form:input/>
可以用于作为输入框,并且可以在转发之后,复现上一次输入框内容。<form:errors/>
可以显示验证结果。modelAttribute="people"
指定绑定的模型属性。<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>validation jsp</title> </head> <body> <form:form method="post" action="${pageContext.request.contextPath}/validationTest" modelAttribute="people"> 电子邮件:<form:input path="email"/> <form:errors path="email"/> <br/> 姓氏:<form:input path="lastName"/><form:errors path="lastName"/><br/> 生日:<form:input path="birthday"/><form:errors path="birthday"/><br/> 计划:<form:input path="plan"/><form:errors path="plan"/><br/> <input type="submit" value="提交"> </form:form> </body> </html>