文章目录
- 夯实Spring系列|第二十章:Spring 类型转换(Type Conversion)
- 1.项目环境
- 2.Spring 类型转换的实现
- 3.使用场景
- 4.基于 JavaBeans 接口的类型转换
- 5.Spring 内建 PropertyEditor 扩展
- 6.自定义 PropertyEditor 扩展
- 6.1 示例
- 7.Spring PropertyEditor 的设计缺陷
- 8.Spring 3 通用类型转换接口
- 9.Spring 内建类型转换器
- 10.Converter 接口的局限性
- 11.GenericConverter 接口
- 12.优化 GenericConverter 接口
- 13.扩展 Spring 类型转换器
- 13.1 扩展方式
- 13.2 示例
- 14.统一类型转换服务
- 15.ConversionService 作为依赖
- 15.1 源码分析
- 16.面试题
- 16.1 Spring 类型转换实现有哪些?
- 16.2 Spring 类型转换器接口有哪些?
- 17.参考
夯实Spring系列|第二十章:Spring 类型转换(Type Conversion)
1.项目环境
- jdk 1.8
- spring 5.2.2.RELEASE
- github 地址:https://github.com/huajiexiewenfeng/thinking-in-spring
- 本章模块:conversion
2.Spring 类型转换的实现
1.基于 JavaBeans 接口的类型转换实现(Spring 3.0 之前)
- 基于 java.beans.PropertyEditor 接口扩展
2.Spring 3.0+ 通用类型转换实现
3.使用场景
场景 | 基于 JavaBeans 接口的类型转换实现 | Spring 3.0+ 通用类型转换实现 |
数据绑定 | YES | YES |
BeanWrapper | YES | YES |
Bean 属性类型转换 | YES | YES |
外部化属性类型转换 | NO | YES |
第一,在数据绑定的场景中
- org.springframework.validation.DataBinder#doBind
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
其中 applyPropertyValues 方法将外部的一些配置源转换成 Bean 的属性,中间需要使用类型转换。
第二,在创建 Bean 的过程中(doGetBean)
- AbstractAutowireCapableBeanFactory#doCreateBean
- AbstractAutowireCapableBeanFactory#populateBean
- AbstractAutowireCapableBeanFactory#applyPropertyValues
同样也有 applyPropertyValues 方法,而且实现类似。
4.基于 JavaBeans 接口的类型转换
核心职责
- 将 String 类型的内容转化为目标类型的对象
扩展原理
- Spring 框架将文本内容传递到 PropertyEditor 实现的 setAsText(String) 方法
- PropertyEditor#setAsText(String) 方法实现将 String 类型转化为目标类型的对象
- 将目标类型的对象传入 PropertyEditor#setValue(Object) 方法
- PropertyEditor#setValue(Object) 方法实现需要临时存储传入对象
- Spring 框架将通过 PropertyEditor#getValue() 获取类型转换后的对象
示例
- PropertyEditor 实现,一般继承 PropertyEditorSupport 进行实现
public class StringToPropertiesPropertyEditor extends PropertyEditorSupport {
// 1.实现 setAsText
public void setAsText(String text) throws java.lang.IllegalArgumentException {
// 2.将 String 转换为 Properties
Properties properties = new Properties();
try {
properties.load(new StringReader(text));
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
// 3.临时存储 properties 对象
setValue(properties);
}
}
测试调用
public class PropertyEditorDemo {
public static void main(String[] args) {
// 模拟 spring framework 操作
String text = "name = 小仙";
PropertyEditor propertyEditor = new StringToPropertiesPropertyEditor();
propertyEditor.setAsText(text);
System.out.printf("类型:%s,值:%s\n", propertyEditor.getValue().getClass(), propertyEditor.getValue());
}
}
执行结果:
类型:class java.util.Properties,值:{name=小仙}
5.Spring 内建 PropertyEditor 扩展
转换场景 | 实现类 |
String - > Byte 数组 | org.springframework.beans.propertyeditors.ByteArrayPropertyEditor |
String -> Char | org.springframework.beans.propertyeditors.CharacterEditor |
String -> Char 数组 | org.springframework.beans.propertyeditors.CharArrayPropertyEditor |
String -> Charset | org.springframework.beans.propertyeditors.CharsetEditor |
String -> Class | org.springframework.beans.propertyeditors.ClassEditor |
String -> Currency | org.springframework.beans.propertyeditors.CurrencyEditor |
… | … |
- CharArrayPropertyEditor 源码
public class CharArrayPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(@Nullable String text) {
setValue(text != null ? text.toCharArray() : null);
}
@Override
public String getAsText() {
char[] value = (char[]) getValue();
return (value != null ? new String(value) : "");
}
}
可以看到实现还是比较简单,继承 PropertyEditorSupport 类,并覆盖 setAsText 和 getAsText 方法。
那么我们也可以改造之前 StringToPropertiesPropertyEditor 类增加 getAsText 方法
public class StringToPropertiesPropertyEditor extends PropertyEditorSupport {
private static final String ENCODING = "utf-8";
// 1.实现 setAsText
public void setAsText(String text) throws java.lang.IllegalArgumentException {
// 2.将 String 转换为 Properties
Properties properties = new Properties();
try {
Reader reader = new InputStreamReader(new ByteArrayInputStream(text.getBytes(ENCODING)));
properties.load(reader);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
// 3.临时存储 properties 对象
setValue(properties);
}
public String getAsText() {
Properties properties = (Properties) getValue();
StringBuilder textBuilder = new StringBuilder();
for (Map.Entry entry :
properties.entrySet()) {
textBuilder.append(entry.getKey()).append("=")
.append(entry.getValue())
.append(System.getProperty("line.separator"));
}
return textBuilder.toString();
}
}
测试
public class PropertyEditorDemo {
public static void main(String[] args) {
// 模拟 spring framework 操作
String text = "name = 小仙";
PropertyEditor propertyEditor = new StringToPropertiesPropertyEditor();
propertyEditor.setAsText(text);
propertyEditor.getAsText();
System.out.printf("类型:%s,值:%s\n", propertyEditor.getValue().getClass(), propertyEditor.getValue());
System.out.printf("getAsText 值:%s\n", propertyEditor.getAsText());
}
}
执行结果:
类型:class java.util.Properties,值:{name=小仙}
getAsText 值:name=小仙
6.自定义 PropertyEditor 扩展
扩展模式
- 扩展 java.beans.PropertyEditorSupport 类
实现 org.springframework.beans.PropertyEditorRegistrar
- 实现 org.springframework.beans.PropertyEditorRegistrar#registerCustomEditors 方法
- 将 PropertyEditorRegistrar 实现注册为 Spring Bean
向 org.springframework.beans.PropertyEditorRegistry 注册自定义 PropertyEditor 实现
- 通用类型实现 registerCustomEditor(java.lang.Class<?>, java.beans.PropertyEditor)
- Java Bean 属性类型实现:registerCustomEditor(java.lang.Class<?>, java.lang.String, java.beans.PropertyEditor)
6.1 示例
自定义 Company 类型的 PropertyEditor 实现,将文本转换为 Company 类型
Company
public class Company {
private String name;
private String address;
...
User 类如下
除了普通字段,我们这里加入了一个 Company 类型字段
public class User {
private Long id;
private String name;
private Integer age;
private Company company;
...
Company 类型转换的 PropertyEditor 自定义实现
/**
* {@link Company} 类型 {@link PropertyEditor} 自定义实现
*
* @author :xwf
* @date :Created in 2020\6\13 0013 19:04
* @see Company
*/
public class CompanyTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
String[] split = text.split(",");
Company company = new Company();
if (split.length > 1) {
company.setName(split[0]);
company.setAddress(split[1]);
} else {
company.setName(text);
}
setValue(company);
}
}
注册我们的自定义配置
public class CustomizedPropertyEditorRegistrar implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
// 1.通用类型转换
// 2.Java Bean 属性类型转换
registry.registerCustomEditor(Company.class, new CompanyTypeEditor());
}
}
将注册类声明为 Spring Bean,通过 XML 的方式,也可以通过注解
resources/META-INF 目录下 property-editors-context.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
https://www.springframework.org/schema/util/spring-util.xsd">
<!-- 3.将其声明为 Spring Bean-->
<bean id="customPropertyEditorRegistrar" class="com.huajie.thinking.in.spring.conversion.CustomizedPropertyEditorRegistrar"/>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="user" class="com.huajie.thinking.in.spring.conversion.domain.User">
<property name="name" value="xwf"/>
<property name="age" value="18"/>
<property name="company" value="alibaba,wuhan"/>
</bean>
</beans>
测试
public class SpringCustomizedPropertyEditorDemo {
public static void main(String[] args) {
// 创建并启动 BeanFactory 容器
ConfigurableApplicationContext applicationContext =
new ClassPathXmlApplicationContext("classpath:/META-INF/property-editors-context.xml");
User user = applicationContext.getBean("user", User.class);
System.out.println(user);
applicationContext.close();
}
}
执行结果:
User{id=null, name='xwf', age=18, company=Company{name='alibaba', address='wuhan'}}
可以看到 <property name="company" value="alibaba,wuhan"/>
由文本的方式被转化为 Company 类型。
7.Spring PropertyEditor 的设计缺陷
1.违反单一职责原则
- java.beans.PropertyEditor 接口职责太多,除了类型转换,还包括 Java Beans 事件和 Java GUI 交互
2.java.beans.PropertyEditor 实现类型局限
- 来源类型只能为 java.lang.String 类型
- 从上面的例子可以看出使用并不是很方便
3.java.beans.PropertyEditor 实现缺少类型安全
- 处理实现类命名可以表达语义,实现类无法感知目标转换类型
- 使用 Object 作为返回类型,编程时没有强类型约束,运行时可能会出异常
8.Spring 3 通用类型转换接口
为了解决上面 PropertyEditor 的一些问题和缺陷,从 Spring 3 开始,引入了通用类型转换接口,基于 JDK1.5 的泛型进行设计和改良
类型转换接口 - org.springframework.core.convert.converter.Converter<S, T>
- 泛型参数 S:来源类型(Source),参数 T:目标类型(Target)
- 核心方法:T convert(S source)
匹配条件 - org.springframework.core.convert.converter.ConditionalConverter
- boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType)
- 判断传入类型和目标类型是否能匹配到转换器
通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter
- 核心方法:Object convert( Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
- 配对类型:org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
- 多类型的匹配
- 类型描述:org.springframework.core.convert.TypeDescriptor
9.Spring 内建类型转换器
内建扩展分为三类,分别放在三个包下面
转换场景 | 实现类所在包名(package) |
日期/时间相隔 | org.springframework.format.datetime |
Java 8 日期/时间相关 | org.springframework.format.datetime.standard |
通用实现 | org.springframework.core.convert.support |
部分源码截图
从命名可以看出,这些内建的实现,并不局限于 String 转其他类型。
10.Converter 接口的局限性
局限一:缺少 Source Type 和 Target Type 前置判断
- 应对:增加 org.springframework.core.convert.converter.ConditionalConverter
局限二:仅能转换单一的 Source Type 和 Target Type
- 应对:使用 org.springframework.core.convert.converter.GenericConverter
11.GenericConverter 接口
org.springframework.core.convert.converter.GenericConverter
核心要素 | 说明 |
使用场景 | 用于“复合”类型转换场景,比如 Collection、Map、数组等 |
转换范围 | Set<ConvertiblePair> getConvertibleTypes() |
配对类型 | org.springframework.core.convert.converter.GenericConverter.ConvertiblePair |
转换方法 | Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) |
类型描述 | org.springframework.core.convert.TypeDescriptor |
12.优化 GenericConverter 接口
GenericConverter 局限性
- 缺少 Source Type 和 Target Type 前置判断
- 单一类型转换实现复杂
GenericConverter 接口优化 - ConditionalGenericConverter
- 复合类型转换:org.springframework.core.convert.converter.GenericConverter
- 类型条件判断:org.springframework.core.convert.converter.ConditionalGenericConverter
13.扩展 Spring 类型转换器
13.1 扩展方式
第一种扩展方式:实现转换器接口
- org.springframework.core.convert.converter.Converter
- org.springframework.core.convert.converter.ConverterFactory
- org.springframework.core.convert.converter.GenericConverter
第二种扩展方式:注册转换器实现
- org.springframework.core.convert.support.ConversionServiceFactory
- org.springframework.context.support.ConversionServiceFactoryBean
- org.springframework.core.convert.ConversionService
13.2 示例
自定义类实现 Company 类型转化为 String 类型,实现 ConditionalGenericConverter 接口
public class CompanyToStringConverter implements ConditionalGenericConverter {
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return Company.class.equals(sourceType.getObjectType()) &&
String.class.equals(targetType.getObjectType());
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Company.class, String.class));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
Company company = Company.class.cast(source);
return company.getName() + "-" + company.getAddress();
}
}
修改 property-editors-context.xml 文件
- user 中新增 companyAsText 属性,ref 指向 company Bean
- 新增 company Bean 的配置
- 声明 ConversionServiceFactoryBean,将自定义的 CompanyToStringConverter 配置为 ConversionServiceFactoryBean 的属性 converters,这个属性为 Set 集合
- 注意 Xml 中 Bean 的配置 id 必须等于
conversionService
<bean id="user" class="com.huajie.thinking.in.spring.conversion.domain.User">
<property name="name" value="xwf"/>
<property name="age" value="18"/>
<property name="company" value="alibaba,wuhan"/>
<property name="companyAsText" ref="company"/>
</bean>
<bean id="company" class="com.huajie.thinking.in.spring.conversion.domain.Company">
<property name="name" value="alimama"/>
<property name="address" value="hangzhou"/>
</bean>
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="com.huajie.thinking.in.spring.conversion.CompanyToStringConverter"/>
</set>
</property>
</bean>
测试:
public class SpringCustomizedPropertyEditorDemo {
public static void main(String[] args) {
// 创建并启动 BeanFactory 容器
ConfigurableApplicationContext applicationContext =
new ClassPathXmlApplicationContext("classpath:/META-INF/property-editors-context.xml");
User user = applicationContext.getBean("user", User.class);
System.out.println(user);
applicationContext.close();
}
}
执行结果:
User{id=null, name='xwf', age=18, company=Company{name='alibaba', address='wuhan'}, companyAsText=alimama-hangzhou}
可以看到 companyAsText 的属性为 alimama-hangzhou,和我们的 convert 实现一样将 name 和 address 进行拼接。
14.统一类型转换服务
实现类型 | 说明 |
GenericConversionService | 通用 ConversionService 模板实现,不内置转换器实现 |
DefaultConversionService | 基础 ConversionService 实现,内置常用转换器实现 |
FormattingConversionService | 通用 Formatter + GeneircConversionService 实现,不内置 Formatter 实现和转换器实现 |
DefaultFormattingConversionService | DefaultConversionService + 格式化实现 |
15.ConversionService 作为依赖
类型转换器底层接口 - org.springframework.beans.TypeConverter
- 起始版本:Spring 2.0
- 核心方法 - convertIfNecessary 重载方法
- 抽象实现 - org.springframework.beans.TypeConverterSupport
- 简单实现 - org.springframework.beans.SimpleTypeConverter
类型转换器底层抽象实现 - org.springframework.beans.TypeConverterSupport
- 实现接口 - org.springframework.beans.TypeConverter
- 扩展实现 - org.springframework.beans.PropertyEditorRegistrySupport
- 委派实现 - org.springframework.beans.TypeConverterDelegate
15.1 源码分析
- 通过 name =“conversionService” 依赖查找
- AbstractApplicationContext#refresh //应用上下文启动
- AbstractApplicationContext#finishBeanFactoryInitialization //BeanFactory初始化完成方法
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
...
通过 name=“conversionService” + ConversionService.class 类型的方式依赖查找 ConversionService Bean
- BeanFactory 应用上下文设置 conversionService 属性
beanFactory.setConversionService 将第一步查找到的 ConversionService Bean 对象设置到当前 BeanFactory 应用上下文的 conversionService
属性中
- 应用上下启动中的 finishBeanFactoryInitialization 阶段
接着第一步的调用链路继续往下
ConfigurableListableBeanFactory#preInstantiateSingletons
-> AbstractBeanFactory#getBean
-> AbstractBeanFactory#doGetBean
-> AbstractAutowireCapableBeanFactory#doCreateBean
-> AbstractAutowireCapableBeanFactory#instantiateBean
-> AbstractBeanFactory#initBeanWrapper
protected void initBeanWrapper(BeanWrapper bw) {
bw.setConversionService(getConversionService());
registerCustomEditors(bw);
}
-> AbstractBeanFactory#getConversionService
- 这里的 getConversionService 获取的就是第二步中设置的 ConversionService Bean 对象
- 然后将这个 ConversionService 对象传到 BeanWapper 这个对象中
- Bean (populateBean)属性赋值阶段中完成类型转换
-> AbstractAutowireCapableBeanFactory#populateBean //属性赋值 -> 转换(数据来源:PropertyValues)
-> AbstractPropertyAccessor#setPropertyValues
-> TypeConverter#convertIfNecessary
-> TypeConverterDelegate#convertIfNecessary
-> PropertyEditor or ConversionService
16.面试题
16.1 Spring 类型转换实现有哪些?
1.基于 JavaBeans PropertyEditor 接口实现
2.Spring 3.0+ 通用类型转换实现,16.2 中的 4 个接口
16.2 Spring 类型转换器接口有哪些?
- 类型转换接口 - org.springframework.core.convert.converter.Converter
- 通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter
- 类型条件接口 - org.springframework.core.convert.converter.ConditionalConverter
- 综合类型接口 - org.springframework.core.convert.converter.ConditionalGenericConverter
17.参考