文章目錄
- 夯實Spring系列|第二十章:Spring 類型轉換(Type Conversion)
夯實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
- AbstractAutowireCapableBeanFactory#populateBean
同樣也有 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
- 注意 Xml 中 Bean 的配置 id 必須等於
<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.參考
- 極客時間-小馬哥《小馬哥講Spring核心編程思想》