Spring学习笔记:Spring类型转换及源码分析

本文仅供学习交流使用,侵权必删。
不作商业用途,转载请注明出处

1. 概述

在Spring中,我们通常可以使用PropertyEditor将一个字符串类型的属性转换成指定类型的属性。而在Spring 3之后,Spring提供了一种更加通用的类型转换机制不仅限于字符串类型和对象类型之间的转换,同时也提供了多个扩展接口给开发者扩展类型转换功能。
下面将记录下如何在Spring环境中利用这些类型转换接口进行扩展以及类型转换的源码分析

当前使用版本是Spring Framework 5.2.2.RELEASE

2. 扩展类型转换器

2.1 PropertyEditor

这种方式是实现了Java Beans的PropertyEditor的一种实现。如何使用PropertyEditor进行类型转换,下面会做一个示例。
该示例是将一个String类型转换成一个Object类型的PropertyEditor扩展接口。

  • 首先定义User和UserContext
 public class User {
     private String name;
     private UserContext contextList;
 
     public User() {
     }
 
     public String getName() {
         return name;
     }
 
     public void setName(String name) {
         this.name = name;
     }
 
     public UserContext getContextList() {
         return contextList;
     }
 
     public void setContextList(UserContext contextList) {
         this.contextList = contextList;
     }
 }
 
 
 
 public class UserContext {
     private String[] context;
 
     public String[] getContext() {
         return context;
     }
     
     public void setContext(String[] context) {
         this.context = context;
     }
 }
  • 定义一个自定义PropertyEditor实现。在这里可以继承Spring提供的PropertyEditorSupport。该类是Spring Framework中PropertyEditor的默认实现,我们可以利用这个作为一个基类或者是一个委派类帮助我们构建自定义PropertyEditor。
public class StringToUserContextPropertyEditor extends PropertyEditorSupport implements PropertyEditor {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        String[] arr = text.split (",");
        UserContext userContext = new UserContext ();
        userContext.setContext (arr);
        setValue (userContext);
    }


    @Override
    public String getAsText() {
        UserContext userContext = (UserContext) getValue ();
        return Arrays.toString (userContext.getContext ());
    }


}
  • 实现PropertyEditorRegistrar自定义一个PropertyEditor注册器,需要将其加载到容器中并在回调方法中指定需要转换的类型
 public class CustomizedPropertyEditorRegistered implements PropertyEditorRegistrar {
     @Override
     public void registerCustomEditors(PropertyEditorRegistry propertyEditorRegistry) {
         propertyEditorRegistry.registerCustomEditor (UserContext.class, new StringToUserContextPropertyEditor ());
     }
 }
  • 在resources路径下添加XML配置conversion-context.xml,并配置对应的bean信息
 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:util="http://www.springframework.org/schema/util"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        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/utils/spring-util.xsd">
 
     <!-- 通过CustomEditorConfigurer这个BeanPostProcessor将我们的自定义注册器加载到容器中 -->
     <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
         <property name="propertyEditorRegistrars">
             <list>
                 <ref bean="customizedPropertyEditorRegistered"/>
             </list>
         </property>
     </bean>
 
     
     <!-- 自定义注册器 -->
     <bean id="customizedPropertyEditorRegistered"   class="org.kgyam.spring.conversion.propertyEditor.CustomizedPropertyEditorRegistered"/>
 
   <!-- User bean信息 -->
     <bean id="user" class="org.kgyam.domain.User">
         <property name="name" value="dalipapa"/>
         <property name="contextList" value="v1,v2,v3,v4,v5"></property>
     </bean>
 
 </beans>
  • 编写Main方法测试
public class CustomizedPropertyEditorDemo {
    public static void main(String[] args) {
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("classpath:META-INF/conversion-context.xml");
        User user=applicationContext.getBean("user", User.class);
        System.out.println(user);
    }
}

利用PropertyEditor实现类型转换其实存在一定的缺陷:

  1. 首先这种方式只能支持String和Object之间的类型转换
  2. 其次setValue和getValue并没有支持泛型,在每次getValue的时候需要做类型强转,这种写法并不优雅
  3. PropertyEditor包含大量针对GUI的操作,因而从设计上违反了单一职责原则

所以在Spring 3.0之后添加了Converter实现类型转换。


2.2 Converter | ConverterFactory | GenericConverter

Spring 3之后提供了Converter和GenericConverter作为类型转换的扩展接口。

  • org.springframework.core.convert.converter.Converter<S, T>作为转换接口
  • org.springframework.core.convert.converter.GenericConverter也是转换接口
  • org.springframework.core.convert.converter.ConverterFactory<S, R>是Converter的工厂接口
  • org.springframework.core.convert.converter.ConditionalConverter作为转换类型校验接口

以下对于这三种类型接口编写一个自定义类型转换示例,该示例是实现一个Properties类型转换为String类型的扩展接口

  • 首先定义一个LocalProperties类
 public class LocalProperties {
     private String propertiesStr;
 
     public String getPropertiesStr() {
         return propertiesStr;
     }
 
     public void setPropertiesStr(String propertiesStr) {
         this.propertiesStr = propertiesStr;
     }
 
     @Override
     public String toString() {
         return "LocalProperties{" +
                 "propertiesStr='" + propertiesStr + '\'' +
                 '}';
     }
 }
  • Converter实现类
 public class PropertiesToStringConverter implements Converter<Properties, String>, ConditionalConverter {
     @Override
     public String convert(Properties properties) {
         System.out.println ("PropertiesToStringConverter#convert");
         StringBuilder stringBuilder = new StringBuilder ();
         for (Map.Entry<Object, Object> entry : properties.entrySet ()) {
             stringBuilder.append (entry.getKey () + ":" + entry.getValue ()).append (",");
         }
         return stringBuilder.deleteCharAt (stringBuilder.length () - 1).toString ();
     }
 
     @Override
     public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
         System.out.println ("PropertiesToStringConverter#matches");
         return Properties.class.equals (sourceType.getType ()) && String.class.equals (targetType.getType ());
     }
 }
  • ConverterFactory实现类
 public class PropertiesToStringConverterFactory implements ConverterFactory<Properties, String> {
     @Override
     public <T extends String> Converter<Properties, T> getConverter(Class<T> aClass) {
         return new InnerPropertiesToStringConverter ();
     }
 
 
     private static final class InnerPropertiesToStringConverter<T extends String> implements Converter<Properties, String> {
 
         @Override
         public String convert(Properties properties) {
             StringBuilder stringBuilder = new StringBuilder ();
             for (Map.Entry<Object, Object> entry : properties.entrySet ()) {
                 stringBuilder.append (entry.getKey () + ":" + entry.getValue ()).append (",");
             }
             return stringBuilder.deleteCharAt (stringBuilder.length () - 1).toString ();
         }
     }
 }
  • GenericConverter接口和ConditionalConverter接口有一个组合的接口ConditionalGenericConverter,我们直接实现该接口即可满足转换类型的校验和类型转换两个功能
 public class PropertiesToStringGenericConverter implements ConditionalGenericConverter {
 
     @Override
     public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
         return Properties.class.equals (sourceType.getObjectType ())
                 && String.class.equals (targetType.getObjectType ());
     }
 
     @Override
     public Set<ConvertiblePair> getConvertibleTypes() {
         return Collections.singleton (new ConvertiblePair (Properties.class, String.class));
     }
 
     @Override
     public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
         if (source == null) {
             return null;
         }
         Properties properties = (Properties) source;
         StringBuilder textBuilder = new StringBuilder ();
         for (Map.Entry<Object, Object> entry : properties.entrySet ()) {
             textBuilder.append (entry.getKey ()).append ("=").append (entry.getValue ()).append (System.getProperty ("line.separator"));
         }
         return textBuilder.toString ();
     }
 }
  • 在resources路径下添加XML配置conversion-context.xml,并将converter注册到容器中。这里需要的是org.springframework.context.support.ConversionServiceFactoryBean这个bean配置的name必须是conversionService,否则会抛出异常。其中的原因在源码分析中说明
 <?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">
 
 
     <util:properties id="default-properties">
         <prop key="id">pc1628</prop>
         <prop key="port">8080</prop>
         <prop key="ip">127.0.0.1</prop>
     </util:properties>
 
     <bean id="localProperties" class="org.kgyam.spring.conversion.converter.LocalProperties">
         <property name="propertiesStr" ref="default-properties"></property>
     </bean>
 
     <!-- 必须要用conversionService    -->
     <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
         <property name="converters">
             <util:list>
                 <bean class="org.kgyam.spring.conversion.converter.PropertiesToStringConverter"/>
 <!--                <bean class="org.kgyam.spring.conversion.converter.PropertiesToStringGenericConverter"/>-->
 
 <!--                <bean class="org.kgyam.spring.conversion.converter.CollectionToMapConditionalGenericConverter"/>-->
             </util:list>
         </property>
     </bean>
 </beans>
  • 编写Main方法测试
 public class ConverterDemo {
     public static void main(String[] args) {
         ApplicationContext applicationContext = new ClassPathXmlApplicationContext ("classpath:META-INF/conversion-context.xml");
         LocalProperties localProperties=applicationContext.getBean (LocalProperties.class);
         System.out.println (localProperties);
     }
 }

3. 关于Converter和GenericConverter的接口选择

对于单一类型的转换我们可以使用Converter接口,而对于复杂类型转换Spring官方文档更加推荐使用GenericConverter接口,尝试分析下这样选择的理由:

  1. 首先我们看下GenericConverter接口如图3-1所示,GenericConverter接口支持一组ConvertiblePair的类型转换,而ConvertiblePair可以定义一组来源类型和目标类型,如图图3-2。这种情况下我们能在一个GenericConverter接口下配置多个来源目标转换类型,这样接口针对转换类型能更多样更灵活。


    图3-1
图3-2
  1. 而Converter<S,T>接口因为用到的是泛型,如图3-3。所以仅建议用于实现来源单一类型转换成目标单一类型的转换功能,Converter<S,T>的从设计上来说更加符合单一职责原则,但灵活性来说却不如GenericConverter


    图3-3

所以针对不同的类型选择,我们可以有选择性地选用不同的类型转换接口进行扩展

4. Spring关于转换过程的源码分析

下面的源码分析是基于ConditionalGenericConverter的实现进行分析的,这个接口组合了GenericConverter和ConditionalConverter两个接口,如图4-1所示。所以这个组合接口的需要实现的方法有GenericConverter#getConvertibleTypes方法和GenericConverter#convert方法以及ConditionalConverter#matches

图4-1

bean属性的类型转换是在bean创建的时候进行的,接下来会根据上面这三个方法被调用的时机以及其调用堆栈分析类型转换过程。

在这里先给出一个结论:这些方法的调用顺序是getConvertibleTypes->matches->convert。

4.1 GenericConverter#getConvertibleTypes

  • 首先先被调用的是GenericConverter#getConvertibleTypes。这个是在ConversionServiceFactoryBean这个bean被创建的时候被调用的。
图4-1-1
  • ConversionServiceFactoryBean初始化的时候会被回调初始化方法afterPropertiesSet,如图4-1-2所示
图4-1-2
  • 然后afterPropertiesSet方法会调用ConversionServiceFactory#registerConverters将已配置的Converter Bean对象注册到ConversionService中,在这里可以到针对不同的转换器类型会有特定的方法处理,如图4-1-3所示。
    • 可以看到针对Converter<S,T>和ConverterFactory<?,?>类型的处理,如图4-1-5和图4-1-6,它们经过处理后最终都会转换为一个org.springframework.core.convert.support.GenericConversionService.ConverterAdapter,这个类其实是ConditionalGenericConverter的实现类。
    • 注册器最后都是通过ConverterRegistry#addConverter注册转换器到ConversionService,如图4-1-4。看到该方法可以知道ConversionService最后注册的转换器其实都是GenericConverter类型的。
图4-1-3
图4-1-4
图4-1-5
图4-1-6
  • 根据刚才的方法调用堆栈图4-1-1看到,容器会通过调用GenericConversionService#addConverter方法进而调用其内部类Converters#add,从注释上看这个Converters是帮助管理所有注册到当前ConversionService的Converter
图4-1-7
  • 分析下Converters#add方法,该方法首先会获取这个GenericConverter可转换类型集合,然后遍历这个集合,如图4-1-8。然后执行getMatchableConverters返回ConvertiblePair对应的ConvertersForPair对象,如图4-1-9。最后将当前注册的GenericConverter对象添加到ConvertersForPair所管理的GenericConverter集合中,如图4-1-10
图4-1-8
图4-1-9
图4-1-10
  • 在这个方法的调用后,基本就完成了类型转换器的注册了。

4.2 ConditionalConverter#matches

  • 下一个被调用的是matches方法,这个方法是在其他Spring Bean构建过程中的赋值阶段中执行的,在AbstractAutowireCapableBeanFactory#convertForProperty会调用BeanWrapperImpl#convertForProperty,如图4-2-2
图4-2-1
图4-2-2
  • 其中BeanWrapperImpl#convertForProperty方法体里会调用到BeanWrapperImpl里面的成员变量TypeConverterDelegate#convertIfNecessary方法,如图4-2-3。该方法就是类型转换的入口,这TypeConverterDelegate是一个委派类,最终是委派到对应的ConversionService或者PropertyEditor执行类型转换,后面会分析这个类的一些情况,这里暂不做展开讨论。而在这里TypeConverterDelegate会委派GenericConversionService#canConvert方法,这个方法用于检查是否能找到对应的类型转换器
图4-2-3
  • 顺着调用堆栈下去会调用GenericConversionService$Converters#getRegisteredConverter,如图4-2-4。这个方法会获取符合要求的ConvertersForPair对象,并遍历该对象中的GenericConverter集合对象并调用其matches方法,返回符合要求的GenericConverter对象
图4-2-4
图4-2-5
  • 然后调用完matches方法,容器就基本完成了匹配对应转换器的过程。

4.3 GenericConverter#convert

  • 最后被调用的convert方法是类型转换的主要实现逻辑,其实这里就是调用matches方法返回的那个GenericConverter实例的convert的方法。
图4-3-1
图4-3-2
  • 不过这里并不是直接调用,而是通过conversionService中使用工具类进行调用的,如图图4-3-4所示
图4-3-3
图4-3-4
  • 到这里基本就完成一个类型转换的整体过程了,接下来会再分析下留下来的一些问题。

4.4 关于TypeConverter

上面提到TypeConverterDelegate,这个类是TypeConverterSupport的一个成员属性。而这个TypeConverterSupport是什么呢?接下来先展示BeanWrapperImpl的结构图,如图4-4-1所示。

图4-4-1
  • 根据结构图可以看到TypeConverterSupport是BeanWrapperImpl的父类,TypeConverterSupport实现的接口TypeConverter,其主要作用是做类型转换,如图4-4-2所示。它是Spring Framework中类型转换的底层接口,同时需要注意的是TypeConverter本身是非线程安全的。
图4-4-2
  • TypeConverterSupport的成员属性TypeConverterDelegate是在BeanWrapperImpl实例化调用父类构造函数时创建的,如图4-4-3。再观察下TypeConverterDelegate构造函数图4-4-4。这里传入的PropertyEditorRegistrySuppport就是BeanWrapperImpl本身。
图4-4-3
图4-4-4
  • 根据TypeConverterDelegate转换逻辑如图4-4-5以及刚才的构造函数关联起来,我们可以知道PropertyEditor和ConversionService都是从BeanWrapperImpl中获取的,由此可以变相得知BeanWrapperImpl其实自带类型转换的能力,其转换能力就是通过它的成员属性PropertyEditor或者ConversionService来实现的
图4-4-5
  • 那么再来看下BeanWrapperImpl中的ConversionService到底是从哪里传入的。我们可以看下Bean的构建过程中有一个Bean的实例化过程org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#instantiateBean,如图4-4-6
图4-4-6
  • 注意下这里的一个初始化BeanWrapper方法org.springframework.beans.factory.support.AbstractBeanFactory#initBeanWrapper,如图4-4-7。可以看出这里是将BeanFactory中的ConversionService赋值给BeanWrapperImpl的。那么我们也知道这个ConversionService从哪里来的。
图4-4-7

4.5 关于BeanFactory中的ConversionService

在这里看下BeanFactory中的ConversionService到底是什么时候构建的。然后同时解答下在一开始的代码示例中,有提到配置org.springframework.context.support.ConversionServiceFactoryBean这个bean配置的id必须是conversionService,否则会抛出异常。这里我们分析以下源码查找出原因所在。

  • 首先看下方法调用堆栈图4-5-1,这里的关键就是AbstractApplicationContext#refresh调用的AbstractApplicationContext#finishBeanFactoryInitialization方法
图4-5-1
  • 这个方法首先要执行的就是创建ConversionService并存放到BeanFactory中,如图4-5-2。而这里查找ConversionService的beanName是一个字符串常量,而这个字符串常量就是conversionService,如图4-5-3。所以这里就说明了为什么我们在配置ConversionServiceFactoryBean的时候要使用conversionService作为id,否则会抛出异常的原因了。
图4-5-2
图4-5-3
  • 在BeanFactory中有一个成员属性存放这ConversionService,如图4-5-4所示。当BeanWrapper初始化的时候,BeanFactory就会将ConversionService传入BeanWrapper中。
图4-5-4

5. 总结

Spring Framework的类型转换功能通过不断升级迭代从一开始使用Java Beans的PropertyEditor,到Spring 3之后升级成Converter接口和GenericConverter接口。可以看出Spring为了提高类型转换功能的扩展性下了很多工夫。

同时在配置ConversionService的bean时候我们要注意配置id需要是conversionService。虽然在spring官方文档的demo也是使用conversionService作为id,但是文档中貌似没有提醒用户配置时候需要注意这个id的命名,所以这里需要开发者对其源码有稍微了解或者遵循官方文档的demo。

参考文档

Spring Convert官方说明

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容