本文仅供学习交流使用,侵权必删。
不作商业用途,转载请注明出处
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实现类型转换其实存在一定的缺陷:
- 首先这种方式只能支持String和Object之间的类型转换
- 其次setValue和getValue并没有支持泛型,在每次getValue的时候需要做类型强转,这种写法并不优雅
- 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接口,尝试分析下这样选择的理由:
-
首先我们看下GenericConverter接口如图3-1所示,GenericConverter接口支持一组ConvertiblePair的类型转换,而ConvertiblePair可以定义一组来源类型和目标类型,如图图3-2。这种情况下我们能在一个GenericConverter接口下配置多个来源目标转换类型,这样接口针对转换类型能更多样更灵活。
-
而Converter<S,T>接口因为用到的是泛型,如图3-3。所以仅建议用于实现来源单一类型转换成目标单一类型的转换功能,Converter<S,T>的从设计上来说更加符合单一职责原则,但灵活性来说却不如GenericConverter
所以针对不同的类型选择,我们可以有选择性地选用不同的类型转换接口进行扩展
4. Spring关于转换过程的源码分析
下面的源码分析是基于ConditionalGenericConverter的实现进行分析的,这个接口组合了GenericConverter和ConditionalConverter两个接口,如图4-1所示。所以这个组合接口的需要实现的方法有GenericConverter#getConvertibleTypes方法和GenericConverter#convert方法以及ConditionalConverter#matches
bean属性的类型转换是在bean创建的时候进行的,接下来会根据上面这三个方法被调用的时机以及其调用堆栈分析类型转换过程。
在这里先给出一个结论:这些方法的调用顺序是getConvertibleTypes->matches->convert。
4.1 GenericConverter#getConvertibleTypes
- 首先先被调用的是GenericConverter#getConvertibleTypes。这个是在ConversionServiceFactoryBean这个bean被创建的时候被调用的。
- ConversionServiceFactoryBean初始化的时候会被回调初始化方法afterPropertiesSet,如图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-1看到,容器会通过调用GenericConversionService#addConverter方法进而调用其内部类Converters#add,从注释上看这个Converters是帮助管理所有注册到当前ConversionService的Converter
- 分析下Converters#add方法,该方法首先会获取这个GenericConverter可转换类型集合,然后遍历这个集合,如图4-1-8。然后执行getMatchableConverters返回ConvertiblePair对应的ConvertersForPair对象,如图4-1-9。最后将当前注册的GenericConverter对象添加到ConvertersForPair所管理的GenericConverter集合中,如图4-1-10
- 在这个方法的调用后,基本就完成了类型转换器的注册了。
4.2 ConditionalConverter#matches
- 下一个被调用的是matches方法,这个方法是在其他Spring Bean构建过程中的赋值阶段中执行的,在AbstractAutowireCapableBeanFactory#convertForProperty会调用BeanWrapperImpl#convertForProperty,如图4-2-2
- 其中BeanWrapperImpl#convertForProperty方法体里会调用到BeanWrapperImpl里面的成员变量TypeConverterDelegate#convertIfNecessary方法,如图4-2-3。该方法就是类型转换的入口,这TypeConverterDelegate是一个委派类,最终是委派到对应的ConversionService或者PropertyEditor执行类型转换,后面会分析这个类的一些情况,这里暂不做展开讨论。而在这里TypeConverterDelegate会委派GenericConversionService#canConvert方法,这个方法用于检查是否能找到对应的类型转换器
- 顺着调用堆栈下去会调用GenericConversionService$Converters#getRegisteredConverter,如图4-2-4。这个方法会获取符合要求的ConvertersForPair对象,并遍历该对象中的GenericConverter集合对象并调用其matches方法,返回符合要求的GenericConverter对象
- 然后调用完matches方法,容器就基本完成了匹配对应转换器的过程。
4.3 GenericConverter#convert
- 最后被调用的convert方法是类型转换的主要实现逻辑,其实这里就是调用matches方法返回的那个GenericConverter实例的convert的方法。
- 不过这里并不是直接调用,而是通过conversionService中使用工具类进行调用的,如图图4-3-4所示
- 到这里基本就完成一个类型转换的整体过程了,接下来会再分析下留下来的一些问题。
4.4 关于TypeConverter
上面提到TypeConverterDelegate,这个类是TypeConverterSupport的一个成员属性。而这个TypeConverterSupport是什么呢?接下来先展示BeanWrapperImpl的结构图,如图4-4-1所示。
- 根据结构图可以看到TypeConverterSupport是BeanWrapperImpl的父类,TypeConverterSupport实现的接口TypeConverter,其主要作用是做类型转换,如图4-4-2所示。它是Spring Framework中类型转换的底层接口,同时需要注意的是TypeConverter本身是非线程安全的。
- TypeConverterSupport的成员属性TypeConverterDelegate是在BeanWrapperImpl实例化调用父类构造函数时创建的,如图4-4-3。再观察下TypeConverterDelegate构造函数图4-4-4。这里传入的PropertyEditorRegistrySuppport就是BeanWrapperImpl本身。
- 根据TypeConverterDelegate转换逻辑如图4-4-5以及刚才的构造函数关联起来,我们可以知道PropertyEditor和ConversionService都是从BeanWrapperImpl中获取的,由此可以变相得知BeanWrapperImpl其实自带类型转换的能力,其转换能力就是通过它的成员属性PropertyEditor或者ConversionService来实现的
- 那么再来看下BeanWrapperImpl中的ConversionService到底是从哪里传入的。我们可以看下Bean的构建过程中有一个Bean的实例化过程org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#instantiateBean,如图4-4-6
- 注意下这里的一个初始化BeanWrapper方法org.springframework.beans.factory.support.AbstractBeanFactory#initBeanWrapper,如图4-4-7。可以看出这里是将BeanFactory中的ConversionService赋值给BeanWrapperImpl的。那么我们也知道这个ConversionService从哪里来的。
4.5 关于BeanFactory中的ConversionService
在这里看下BeanFactory中的ConversionService到底是什么时候构建的。然后同时解答下在一开始的代码示例中,有提到配置org.springframework.context.support.ConversionServiceFactoryBean这个bean配置的id必须是conversionService,否则会抛出异常。这里我们分析以下源码查找出原因所在。
- 首先看下方法调用堆栈图4-5-1,这里的关键就是AbstractApplicationContext#refresh调用的AbstractApplicationContext#finishBeanFactoryInitialization方法
- 这个方法首先要执行的就是创建ConversionService并存放到BeanFactory中,如图4-5-2。而这里查找ConversionService的beanName是一个字符串常量,而这个字符串常量就是conversionService,如图4-5-3。所以这里就说明了为什么我们在配置ConversionServiceFactoryBean的时候要使用conversionService作为id,否则会抛出异常的原因了。
- 在BeanFactory中有一个成员属性存放这ConversionService,如图4-5-4所示。当BeanWrapper初始化的时候,BeanFactory就会将ConversionService传入BeanWrapper中。
5. 总结
Spring Framework的类型转换功能通过不断升级迭代从一开始使用Java Beans的PropertyEditor,到Spring 3之后升级成Converter接口和GenericConverter接口。可以看出Spring为了提高类型转换功能的扩展性下了很多工夫。
同时在配置ConversionService的bean时候我们要注意配置id需要是conversionService。虽然在spring官方文档的demo也是使用conversionService作为id,但是文档中貌似没有提醒用户配置时候需要注意这个id的命名,所以这里需要开发者对其源码有稍微了解或者遵循官方文档的demo。