夯實Spring系列|第二十章:Spring 類型轉換(Type Conversion)

夯實Spring系列|第二十章:Spring 類型轉換(Type Conversion)

1.項目環境

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

部分源碼截圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pamRyDqo-1593305192568)(G:\workspace\learn-document\spring-framework\csdn\image-20200613202504892.png)]
從命名可以看出,這些內建的實現,並不侷限於 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 源碼分析

  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

  1. BeanFactory 應用上下文設置 conversionService 屬性

beanFactory.setConversionService 將第一步查找到的 ConversionService Bean 對象設置到當前 BeanFactory 應用上下文的 conversionService 屬性中

  1. 應用上下啓動中的 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 這個對象中
  1. 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核心編程思想》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章