Spring 类型转换的实现
spring 有新老两套实现
- 基于 JavaBeans 接口的类型转换实现,java.beans.PropertyEditor接口扩展
- spring 3.0 通用类型转换,
使用场景
场景 | 基于JavaBeans | Spring 3.0+ |
---|---|---|
数据绑定 DataBinder | y | y实现 PropertyEditorRegistry |
BeanWrapper | y | y实现ConfigurablePropertyAccessor#setConversionService |
Bean 属性转换 | y | y |
外部化属性类型转换 | n(springboot会用到) | y |
DataBinder
DataBinder实现了 PropertyEditorRegistry 和 TypeConverter 接口,分别支持两种实现。
BeanWrapper
BeanWrapper 通常不会直接使用,一般是框架通过 DataBinder 或者 BeanFactory 使用。
ps: 在 BeanFactory 中 bean 创建的过程会通过 BeanWrapper 创建对象,可以查看 #doCreateBea -> #populateBean ->
// 通过 PropertyValues 设置属性
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
...
if (pvs != null) {
applyPropertyValues(beanName, mbd, bw, pvs);
}
...
}
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
...
try {
bw.setPropertyValues(mpvs);
return;
}
...
}
ps: DataBinder 也是通过 apply 的方式把 propertyValues 设置到bean上。
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
///
BeanWrapper 的父接口 ConfigurablePropertyAccessor 在3.0的时候增加了 setConversionService 方法设置服务类,并且通过方法setExtractOldValueForEditor设置在设置 property editor 时是否抽取旧值。
父类 PropertyEditorRegistrySupport 保存 ConversionService 变量,并且实现了 PropertyEditorRegistry 接口,说明支持新旧两种实现,是功能的一种整合。
基于 Javabeans 接口的类型转换
核心职责:将String 类型的内容转化为目标类型的对象
扩展原理:
- spirng 框架将文本内容传递到 PropertyEditor 实现的 setAsText 方法
- PropertyEditor#setAsText方法实现将 String 转化为目标类型对象
- 将目标类型的对象传入PropertyEditor#setValue(Object)方法
- PropertyEditor#setValue(Object)方法实现需要临时存储传入对象,有临时存储中间转换对象的能力
- Spring 框架通过PropertyEditor#getValue 获取类型转换后的对象
ps : demo 代码:conversion.String2PropertiesEditor
public class String2PropertiesEditor extends PropertyEditorSupport {
/**
* 原始值
*/
String text;
@Override
public void setAsText(String text) throws IllegalArgumentException {
this.text = text;
Properties properties = new Properties();
try {
properties.load(new StringReader(text));
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
setValue(properties);
}
@Override
public String getAsText() {
return this.text;
}
}
public class PropertyEditorDemo {
public static void main(String[] args) {
String text = "name=why";
PropertyEditor propertyEditor = new String2PropertiesEditor();
propertyEditor.setAsText(text);
// 获取转换后的 property 对象
System.out.println("propertyEditor.getValue() = " + propertyEditor.getValue());
// 获取原始 text
System.out.println("propertyEditor.getAsText() = " + propertyEditor.getAsText());
}
}
自定义 PropertyEditor 扩展,添加到 spring 框架中
Spring 内建 PropertyEditor 内建扩展都在 org.springframework.beans.propertyeditors 包下,下面看下如何自定义。
- 扩展模式:扩展 PropertyEditorSupport
- 实现 org.springframework.beans.PropertyEditorRegistrar
- 实现 registerCustomEditors
- 将 PropertyEditorRegistor 实现 注册为 Spring bean
- 向 PropertyEditorRegistry 注册自定义 PropertyEditor 实现
- 通用类型实现 registerCustomEditor(class,PropertyEditor)
- Java Bean 属性类型实现:热狗is投入CustomEditor(class,PropertyEditor)
public class DIYPropertyEditorRegistrar implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
// 1 类型转换器
String2PropertiesEditor propertyEditor = new String2PropertiesEditor();
// 2 注册属性转换
registry.registerCustomEditor(User.class, "context", propertyEditor);
// 3 DIYPropertyEditorRegistrar 定义为 spring bean 对象
//
}
}
xml 配置如下:
<bean class="conversion.DIYPropertyEditorRegistrar"/>
<bean id="user" class="pojo.User">
<property name="id" value="1"/>
<property name="context">
<value>
id=1
name=why
</value>
</property>
</bean>
demo 程序
public static void main(String[] args) {
ConfigurableApplicationContext context = new ClassPathXmlApplicationContext(
"META-INF/conversion/property-editor.xml");
User user = context.getBean("user", User.class);
// context 属性会注入
System.out.println(user);
context.close();
}
Spring PropertyEditor的设计缺陷
- 违反单一职责原则
PropertyEditor 接口的职责太多,除了类型转换,还有事件和GUI程序的交互逻辑 - PropertyEditor 实现类型局限
源类型只能为 String 类型 - 缺少类型强类型转换
除了实现类的名称可以表达语义(如CharsetEditor),实现类无法感知目标转换类型。setValue(Object value)可以设置任意类型的对象,同样public Object getValue() 返回的是 Object 类型,也不知道具体是什么类型。
Spring 3 通用类型转换接口
- 增加类型转换接口 Converter<S,T>,实现要求线程安全。
- 核心方法 T convert(S s);
- 局限性
- 缺少前置判断。如果判断convert方法的入参是否支持类型转换,不支持的时候返回null,也可以实现类似的功能。但是接口的职责就不单一了,而且返回值有二义性。小比较来说由 ConditionalConverter 实现职责更合理。
- 仅支持一对一转换。由 GenericConverter 代替,复合类型的实现。
Converter 相较 PropertyEditor 增加了原和目标的泛型,支持更广泛的类型转换。因为是泛型,会有泛型擦写的问题,在运行时我们并不一定明确的知道具体的类型,所以有了 GenericConverter 。
-
GenericConverter
- 核心方法 convert(Object, TypeDescriptor,TypeDescriptor)
- 配对类型 GenericConverter.ConvertiblePair,保存原和目标的一对值。支持多个键值对。
- TypeDescriptor,类型的描述,不只是class,还有原生类型,也可能是泛型,比较复杂。
ConditionalConverter。可以让Converter, GenericConverter,ConverterFactory 根据条件执行。boolean matches(TypeDescriptor sourceType, TypeDe scriptor targetType) 方法参数使用的是 TypeDescriptor 更加抽象,可以更好的描述类型。
spring 内建类型转换器
转换场景 | 实现类 |
---|---|
时间/日期 | format.datetime |
joda 时间/日期 | format.datetime.joda |
java8 时间/日期 | format.datetime.standard |
通用实现 | core.convert.support |
可以看下每个包里的实现类。如:StringToArrayConverter。
GenericConverter 接口
核心要素 | 说明 |
---|---|
使用场景 | 可用于复合类型转换场景,如 Collection,Map,数组等 |
转换范围 | Set<ConvertiblePair> getConvertibleTypes(); |
配对类型 | GenericConverter.ConvertiblePair |
转换方法 | Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType); |
类型描述 | org.springframework.core.convert.TypeDescriptor |
GenericConverter 会和 Converter 配合使用。如果转换的是集合,集合中的元素会使用 Converter 转换。看一下 CollectionToArrayConverter 的实现。
// Collection 支持泛型,保存数据都是用的 Object[]。所以这里原核目标都是object
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Collection.class, Object[].class));
}
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
// 类型转换
Collection<?> sourceCollection = (Collection<?>) source;
TypeDescriptor targetElementType = targetType.getElementTypeDescriptor();
Assert.state(targetElementType != null, "No target element type");
Object array = Array.newInstance(targetElementType.getType(), sourceCollection.size());
int i = 0;
for (Object sourceElement : sourceCollection) {
// 每个元素调用单独转换,convert 方法用到了 Converter 接口的实现
Object targetElement = this.conversionService.convert(sourceElement,
sourceType.elementTypeDescriptor(sourceElement), targetElementType);
Array.set(array, i++, targetElement);
}
return array;
}
优化 GenericConverter 接口
-
局限性
- 缺少S和T的前置判断,这一点和Converter一样
- 单一类型转换复杂。从 CollectionToArrayConverter 可以看到每个元素逐个转换,转换依赖 Converter。
-
优化,条件话的接口 ConditionalGenericConverter
- 接口继承 GenericConverter, ConditionalConverter
- spring 内部实现都是实现此接口,如CollectionToArrayConverter
- 如果我们要扩展,也要实现此接口。
统一类型转换服务
org.springframework.core.convert.ConversionService,主要的实现类继承关系如下图:
- GenericConversionService
通用模板实现,不内置转化器实现。
public class GenericConversionService implements ConfigurableConversionService {
// 转化器集合
private final Converters converters = new Converters();
// 保存转化器容器缓存
private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);
}
GenericConversionService 实现了 ConfigurableConversionService 接口,说明是可配置的,converters 保存转化器,模板方法都提供了所有的操作方法。
- DefaultConversionService
基础实现,内置常用转化器实现。
public class DefaultConversionService extends GenericConversionService {
// 单例
private static volatile DefaultConversionService sharedInstance;
public DefaultConversionService() {
// 构造方法添加转化器
addDefaultConverters(this);
}
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
}
DefaultConversionService 继承自 GenericConversionService ,使用继承自模板类的能力添加单个对象和集合对象的转化器,实现了 sharedInstance 单例方式。
FormattingConversionService
通用FOrmatter+GenericConversionService实现,不内置转化器实现。增加对 spring 内部实现的支持,如:PrinterConverter,ParserConverter。DefaultFormattingConversionService
DefaultConversionService +格式化实现,如JSR-354 Money,Currency,JSR-310 Date-Time。
spring web 实现了 WebConversionService ,扩展了更多的类型支持,基本上大同小异。
总结
bean 创建的过程和TypeConversionService结合的主体流程:
- 容器初始化时保存conversionService对象,AbstractApplicationContext#finishBeanFactoryInitialization,查找CONVERSION_SERVICE_BEAN_NAME,并设置到 beanFactory
- bean创建过程:
- BeanDefination,通过xml或者扫描的方式
- bean 实例会转换成 BeanWrapper,
- AbstractBeanFactory#initBeanWrapper 设置BeanWrapper的 ConversionService 对象
- 数据来源是 PropertiesValues
- bw.setPropertyValues(mpvs);
- TypeConverter#converIfNecessnary
- TypeConverterDelegate#converIfNecessnary
- PropertyEditor 或者 ConversionService
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
-
BeanWrapper使用的类型转换实现:
beanWrapper
BeanWrapper 继承了TypeConverterSupport,在初始化的时候会给typeConverterDelegate赋值。
this 是当前BeanWrapper,因为继承了PropertyEditorRegistrySupport,所以有 PropertyEditor 和 ConversionService 信息。
这样 BeanWrapper 和 PropertyEditor 和 ConversionService 就关联起来了。