类型转换可以说是一个框架的基础性功能,struts2,mybatis等开源软件都有自己的类型转换模块,之所以将类型转换作为Spring源码架构分析系列博客的第一篇,是因为我觉得这个模块在Spring众多模块中是处于最底层的,它只被其它模块引用,而不存在对其它模块的依赖,柿子找软的捏,我们就拿它祭旗吧~~。
先上一张鸟瞰图,总体结构是不是很简单,TypeConverter是对外提供服务的接口,其内部主要由两部分组成,分别是类型转换服务和类型转换器。
一. 先介绍类型转换器相关的重要接口
1. 转换器接口
该接口很简单,就定义了一个方法,把对象从源类型转换成目标类型。在Spring3.1.0版本中具体的实现类就很多了,如下面的转换器用于把字符串转换成布尔类型.
StringToBooleanConverter implements Converter<String, Boolean>
2. 转换器工厂接口
这个接口的作用是为了减少转换器个数,减少冗余代码。接口中只定义了一个根据目标类型获取转换器的方法, 所获取的转换器有一个特点,就是源类型是固定的(S),但是目标类型是可变的(R及R的派生类),也就是说从工厂中获取的转换器支持把源类型转换成多种目标类型。设想如果没有转换器工厂,对于String->Big、String->Float、 String->Double....,这得开发多少个转换器啊,并且转换逻辑又是相似的,使用工厂一个搞定ConverterFactory<String,Number>,是不是方便多了。
在Spring3.1.0版本中有4个实现类, "字符转数值", "数值转数值","字符串转换数值", "字符串转枚举"。
a. CharacterToNumberFactory implements ConverterFactory<Character, Number>
b. NumberToNumberConverterFactory implements ConverterFactory<Number, Number>
c. StringToNumberConverterFactory implements ConverterFactory<String, Number>
d. StringToEnumConverterFactory implements ConverterFactory<String, Enum>
3. 格式化接口
格式化接口本身是一个空接口,未定义任何方法,真正定义方法是它的两个超接口(解析器和打印器)。解析器和打印器属于特殊的转换器,可以从两个方面来理解这个特殊性。第一解析器的源类型和打印器的目标类型是固定不变的字符串类型;第二多用于带有格式化属性的类型转换,哪些数据类型带有格式化属性呢,最常见的就是日期和数值,同样的一个java日期对象,可以使用不同的格式把它转换成字符串形式。
在Spring3.1.0版本中主要有两类实现了格式化接口的对象。
a. 数值的格式化,又可细分为货币格式化对象,百分比格式化对象, 普通数值格式化对象.。
b. 日期格式化,对JodaDate有依赖,必须导入该jar包。
4. 基于注解的格式化工厂接口
这个接口的作用是解析注解对象,从而创建相应的格式化对象。在Spring3.1版本中只有两个跟格式化相关的注解对象,分别是"@NumberFormat","@DateTimeFormat"。比如"@NumberFormat"有个枚举类型的属性Style,当该属性值为"CURRENCY"时,格式化工厂就会为该字段使用货币格式化对象进行类型转换。
下面来分析下接口的三个方法:
a. 获取可以被注解的字段类型集合:比如格式化注解@NumberFormat,它可以注解的字段类型有(Short|Integer|Long|Float|Double|BigDecimal|BigInteger),该方法用于校验被注解的字段类型是否合法。
b. 获取打印器 :根据注解对象获取打印器,比如某个Double类型的字段,被@NumberFormat注解了,默认情况会创建NumberFormatter格式化对象用于该字段的类型转换。
c. 获取解析器 : 同上。
在Spring3.1.0版本中有三个基于注解的格式化工厂对象:
a. NumberFormatAnnotationFormatterFactory
b. JodaDateTimeFormatAnnotationFormatterFactory
c. NoJodaDateTimeFormatAnnotationFormatterFactory : 这个只能算是占位置的,用于未引入JodaDate's jar包的情况,无法获取格式化对象,会直接抛出异常。
5. 通用转换器接口
上面介绍的4种接口, 各有各的作用,虽然相互所有关联,但毕竟是4个独立的接口,并不方便外部调用。对于外部调用而言最好是只有一个接口,通用转换器就是一个对外提供服务的门面接口。
该接口及派生接口一共定义了三个方法,简单分析下:
- 获取"可转换类型对"集合: 所谓类型对就是源类型+目标类型,也就是说这个转换器支持哪几种类型之间的互转。
- 执行类型转换: 需要强调的是对于直接实现了该接口的转换器而言,这个方法仅仅实现了部分类型转换逻辑,最终还是依赖"转换器接口"、"转换器工厂接口"、"格式化接口"实现完整转换逻辑。
- 是否支持从源类型到目标类型的转换: 相当于是一个校验方法。
有了门面接口后,如何使它和其它转换器接口产生关联呢,答案是使用适配器。适配器就像双面胶,一手拉着面子(适配器自身实现了通用转换器接口),一手拉着里子(持有其它转换器对象),从而使两者产生了关联。
请看下图,通过适配器可以使用统一的接口(即通用转换器接口)来调用其它转换器的功能。
二. 接着介绍类型转换服务的功能和结构
总的来说转换服务的作用是管理通用转换器(注意是通用转换器),所谓管理具体是指注册通用转换器、获取通用转换器、删除通用转换器。
转换服务在初始化时会把Spring所有默认实现的转换器对象、转换器工厂对象、格式化对象、基于注解的格式化工厂对象注册到Map对象中,即把它们缓存起来,我们姑且叫它缓存A。当收到转换请求时,转换服务会根据源类型描述符和目标类型描述符从缓存A中获取通用转换器,然后把通用转换器添加到缓存B中(也是一个Map对象),为什么这么处理呢? 因为根据源类型描述符和目标类型描述符获取转换器存在一定的性能开销,比如根据传入的源类型从缓存A中找不到通用转换器,Spring会尝试根据源类型的超类型和超接口再次去缓存A中查找。为了避免重复的性能开销,所以要把得到的通用转换器置入缓存B中。
有个地方要特别说明下,如果从缓存A中获取的通用转换器是“基于注解的解析器工厂适配器”或“基于注解的打印器工厂适配器“,它们可以从源类型描述符或目标类型描述符获取注解对象,然后根据注解对象获取格式化对象,同样它们也会把获得的打印器或解析器置入缓存C或缓存D中,目的也是为了避免下次再次解析注解对象的性能开销。
下面图是转换服务接口体系
三. 总结
基本上把Spring类型转换模块的内容讲清楚了,好久没写作文了,写作文真的不比写代码轻松,修改了好几遍基本算是让自己满意了,希望这篇文章能对大家有所帮助吧。
可能细心的读者会发现,我还漏了一块内容,就是“PropertyEditor”,其实我是故意不想引入这块内容,因为Spring之所以还保留PropertyEditor更多的是为了兼容2.5之前的版本,而且如果引入了PropertyEditor会让整个类型转换模块的结构变得不那么漂亮了。如果你想借鉴Spring的结构去开发自己的类型转换模块完全可以不用考PropertyEditor。