Spring 类型转换

Spring 类型转换的实现

spring 有新老两套实现

  1. 基于 JavaBeans 接口的类型转换实现,java.beans.PropertyEditor接口扩展
  2. spring 3.0 通用类型转换,

使用场景

场景 基于JavaBeans Spring 3.0+
数据绑定 DataBinder y y实现 PropertyEditorRegistry
BeanWrapper y y实现ConfigurablePropertyAccessor#setConversionService
Bean 属性转换 y y
外部化属性类型转换 n(springboot会用到) y

DataBinder

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

BeanWrapper 的父接口 ConfigurablePropertyAccessor 在3.0的时候增加了 setConversionService 方法设置服务类,并且通过方法setExtractOldValueForEditor设置在设置 property editor 时是否抽取旧值。


ConfigurablePropertyAccessor

父类 PropertyEditorRegistrySupport 保存 ConversionService 变量,并且实现了 PropertyEditorRegistry 接口,说明支持新旧两种实现,是功能的一种整合。


PropertyEditorRegistrySupport

基于 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,主要的实现类继承关系如下图:


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赋值。


setWrappedInstance

this 是当前BeanWrapper,因为继承了PropertyEditorRegistrySupport,所以有 PropertyEditor 和 ConversionService 信息。
这样 BeanWrapper 和 PropertyEditor 和 ConversionService 就关联起来了。

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