【重写SpringFramework】第一章beans模块:类型转换(chapter 1-2)
1. 前言
BeanFactory
是和 Bean 打交道的,Bean 实际上就是一个对象。既然是对象,那么就可能涉及到类型转换的问题。同时,还需要为对象的属性赋值,而赋值的过程也会使用类型转换。因此我们需要先了解类型转换和属性访问这两个基本功能,其中属性访问是以类型转换为基础的。本节我们先讨论类型转换功能,初步了解 Spring 是如何组织代码的。
2. 整体结构
我们在分析一个功能时,先来看它的整体结构,只有养成看类图的习惯,才能建立起全局意识。类型转换的继承体系可以分为三组,一是 JDK 提供的属性编辑器,二是 Spring 核心包提供的转换服务,它们都是完成类型转换工作的具体组件,下面会详细说明。第三组是本节需要实现的 API,使用蓝色标识,简单介绍如下:
TypeConverter
:顶级接口,定义了类型转换的相关方法TypeConverterSupport
:类型转换的核心类,几乎所有转换逻辑都由该类实现SimpleTypeConverter
:简单实现类,使用DefaultConversionService
作为转换服务的实例PropertyEditorRegistry
:定义了注册和查找自定义的属性编辑器的方法PropertyEditorRegistrySupport
:持有一组属性编辑器,除了 Spring 默认的属性编辑器之外,用户可以添加自定义的属性编辑器
注:类图与源码的结构有一定的区别。比如,源码中
TypeConverterSupport
将类型转换的具体工作委托给TypeConverterDelegate
处理。我们省略了TypeConverterDelegate
这个类,使得结构更加的紧凑。之后的内容也经常出现这种情况,如无特殊情况不再额外说明。本教程旨在尽量简化不必要的代码,仅保留核心逻辑,如有疑问请参考 Spring 源码。
3. 转换服务
转换服务是 Spring 核心包提供的,先在工程中引入依赖,我们选择的是 Spring4 的最后一个发布版。有关版本的选择已经在序言中解释过了,在学习本教程的过程中,读者可以随时对照源码。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.25.RELEASE</version>
</dependency>
转换服务大体可以分为两个部分。一是服务类,ConversionService
接口及其实现类 DefaultConversionService
负责对外提供服务。二是转换器类,Spring 提供了大量的转换器,它们是 Converter
、ConditionalConverter
、GenericConverter
等接口的实现类。下图为 Converter
接口的主要实现类。
转换器类负责基本类型、时间日期、字符集、数组、集合、Map 等类型之间的转换。由于转换器类的数量众多,仅列举几种比较典型的,如下所示:
StringToNumberConverterFactory.StringToNumber
:字符串转数值ObjectToStringConverter
:对象(包括数值)转字符串StringToCollectionConverter
:将使用逗号分隔的字符串转集合,比如“A,B,C”
转成List<String>
。CollectionToStringConverter
:将集合转成逗号分隔的字符串ArrayToArrayConverter
:将一种类型的数组转换成另一种类型的数组,比如int[]
转String[]
可以看到,转换器类只能完成单向转换,因此涉及两个类型的转换器往往是成对出现的,比如字符串和 Collection
的互相转换。对于数组、集合这种包含多个元素的类型,需要从两个方面考虑。一是外层容器类型的转换,比如数组与集合的互转,其转换器也是成对的。二是内层元素类型的转换,一个转换器就够了,如上面列举的 ArrayToArrayConverter
。
4. 属性编辑器
4.1 Java Bean
Java Bean 是一种结构简单的类,它们通常拥有一组字段,以及相应的取值/赋值方法,很少或几乎没有其他的业务方法。Java Bean 有着广泛的应用,比如用来映射数据库中的表结构、配置文件中的属性、网络接口的请求参数等。取值方法又称 getter 方法,赋值方法又称 setter 方法,方法名有着严格的限制,由 get/set 加上字段名(首字母大写)组成。下面的示例代码是 Java Bean 的典型结构。
//示例代码:Java Bean
public class User {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
4.2 内省
JDK 提供了用于操作 Java Bean 的相关 API,称为「内省」。简单来说,内省(introspection)可以看做是反射(reflection)的子集。我们知道反射是可以直接访问字段,这种操作是比较危险的,破坏了对象的封装性。虽然内省底层使用的还是反射,但不会直接操作字段,而是通过 getter/setter 方法安全地访问字段。内省提供了很多 API,仅列举比较重要的几个:
Introspector
:作为门面类,提供了一些常用功能,比如获取 Java Bean 的相关信息,使用BeanInfo
来描述BeanInfo
:表示一个类的信息,包括方法描述符、属性描述符等PropertyDescriptor
:描述一个 Java Bean 属性的一组读方法和写方法PropertyEditor
:允许对某个属性进行编辑,当属性是一个对象时,赋值和取值的过程中完成了类型转换
我们主要关心属性编辑器的实现。PropertyEditor
接口定义了一组访问属性的方法,子类 PropertyEditorSupport
主要实现了 setValue
和 getValue
方法。自定义的属性编辑器需要继承 PropertyEditorSupport
,并重写 setAsText
和 getAsText
方法。
public interface PropertyEditor {
void setValue(Object value);
Object getValue();
void setAsText(String text) throws IllegalArgumentException;
String getAsText();
}
4.3 代码实现
Spring 实现了一系列属性编辑器,我们从源码中拷贝了四个常用的属性编辑器,存放在 cn.stimd.spring.beans.propertyeditors
目录下。
这些类比较简单,以 ClassEditor
为例进行说明。该类实现了 setAsText
方法,作用是将字符串转换成 Class
对象。然后调用 getValue
方法得到 Object
类型的对象,再强转为 Class
类型,从而完成类型转换的工作。同样地,我们也可以调用 setValue
方法和 getAsText
方法,实现 Class
对象到字符串的转换。由此可见,属性编辑器是双向转换,这一点与转换器不同。
public class ClassEditor extends PropertyEditorSupport {
private final ClassLoader classLoader;
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text)) {
setValue(ClassUtils.resolveClassName(text.trim(), this.classLoader));
}
else {
setValue(null);
}
}
@Override
public String getAsText() {
Class<?> clazz = (Class<?>) getValue();
if (clazz != null) {
return ClassUtils.getQualifiedName(clazz);
}
else {
return "";
}
}
}
PropertyEditorRegistry
接口的作用是管理属性编辑器,registerCustomEditor
方法只能注册用户自定义的属性编辑器,至于 Spring 自带的属性编辑器则是默认加载的。
public interface PropertyEditorRegistry {
void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor);
PropertyEditor findCustomEditor(Class<?> requiredType);
}
PropertyEditorRegistrySupport
类实现了 PropertyEditorRegistry
接口,持有两个属性编辑器的集合,defaultEditors
字段用来存放 Spring 默认的属性编辑器,customEditors
字段用来存放自定义的属性编辑器。当调用 getDefaultEditor
方法时,会检查默认的属性编辑器集合是否存在,如果不存在,则会注册默认的属性编辑器。
public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
private Map<Class<?>, PropertyEditor> defaultEditors;
private Map<Class<?>, PropertyEditor> customEditors = new LinkedHashMap<>(16);
//注册Spring定义的属性编辑器
private void createDefaultEditors() {
this.defaultEditors = new HashMap<>(64);
this.defaultEditors.put(Class.class, new ClassEditor());
//默认的集合编辑器,可以被自定义编辑器覆盖
this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
//Spring的自定义布尔值编辑器除了true和false外,还支持on/off、yes/no、0/1等形式
this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
//JDK没有提供数值包装类型的编辑器,使用Spring自定义数值编辑器来代替
this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
......
}
//获取指定类型的属性编辑器
public PropertyEditor getDefaultEditor(Class<?> requiredType) {
if (this.defaultEditors == null) {
createDefaultEditors();
}
return this.defaultEditors.get(requiredType);
}
}
5. 类型转换器
5.1 TypeConverter
TypeConverter
接口定义了类型转换的方法,这三个方法是重载方法,在细节上有区别。第一个方法有两个参数,value
表示待转换的对象,requiredType
表示转换后的类型,该方法的作用是将对象转换成指定类型。在某些情况下,转换后的对象需要赋值给方法的参数或字段,而方法参数和字段也有自己的类型,还需要进行对比。因此,另外两个重载方法,除了进行类型转换,还需要检查转换后的类型,与方法参数或字段的类型是否一致。
public interface TypeConverter {
//将对象转换为指定的类型
<T> T convertIfNecessary(Object value, Class<T> requiredType) throws TypeMismatchException;
//将对象转换成指定类型,并检查转换后的类型是否与方法参数的类型一致
<T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam) throws TypeMismatchException;
//将对象转换成指定类型,并检查转换后的类型是否与字段的类型一致
<T> T convertIfNecessary(Object value, Class<T> requiredType, Field field) throws TypeMismatchException;
}
5.2 TypeConverterSupport
TypeConverterSupport
是一个抽象类,继承了 PropertyEditorRegistrySupport
类,同时持有一个 ConversionService
实例,说明它拥有使用转换器和属性编辑器的能力。值得注意的是,TypeConverter
接口定义的三个方法最终都调用了同一个重载方法,接下来我们重点分析这个方法。
public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter {
ConversionService conversionService;
@Override
public <T> T convertIfNecessary(Object value, Class<T> requiredType) throws TypeMismatchException{
return convertIfNecessary(value, requiredType, TypeDescriptor.valueOf(requiredType) );
}
@Override
public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam) throws TypeMismatchException{
return convertIfNecessary(value, requiredType, new TypeDescriptor(methodParam));
}
@Override
public <T> T convertIfNecessary(Object value, Class<T> requiredType, Field field) throws TypeMismatchException {
return convertIfNecessary(value, requiredType, new TypeDescriptor(field));
}
//处理类型转换的主方法
public <T> T convertIfNecessary(Object newValue, Class<T> requiredType, TypeDescriptor typeDescriptor) throws TypeMismatchException {
//略
}
}
5.3 convertIfNecessary 方法
第一步,尝试使用转换器来处理。首先检查是否支持从源类型到目标类型的转换,如果支持则调用 ConversionService
的 convert
方法处理。需要注意的是,如果自定义的属性编辑器存在,那么跳过转换器的处理,直接进入第二步。
public <T> T convertIfNecessary(Object newValue, Class<T> requiredType, TypeDescriptor typeDescriptor) throws TypeMismatchException {
//1. ConversionService转换
PropertyEditor editor = findCustomEditor(requiredType);
if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
}
}
第二步,尝试使用属性编辑器处理。优先使用自定义的属性编辑器,如果没找到则查找默认的属性编辑器。如果属性编辑器存在,则调用 setAsText
或 setValue
方法赋值。这时属性编辑器内部已经创建了目标类型的实例,还需要调用 getValue
方法取出实例。
public <T> T convertIfNecessary(Object newValue, Class<T> requiredType, TypeDescriptor typeDescriptor) throws TypeMismatchException {
//1. ConversionService转换(略)
//2. PropertyEditor转换
if(editor == null){
editor = getDefaultEditor(requiredType);
}
if(editor!= null){
if(newValue instanceof String){
editor.setAsText((String) newValue);
}else{
editor.setValue(newValue);
}
return (T) editor.getValue();
}
}
第三步,特殊类型的转换。框架之所以是框架,其中一点是其超强的兼容性,要面对各种复杂的情况。有的时候,转换器和属性编辑器还不足以涵盖所有情况,特别对于一些复杂的类型来说。Spring 考虑到了这一点,针对特殊的情况也给出了解决方案。由于涉及的类型众多,为了简化代码,此处只实现了数组转数组这一种情况,如需了解更多的详情,请参考源码。
我们以 String[]
转 Class[]
为例说明,虽然 Spring 提供了 ArrayToArrayConverter
,但仅支持部分类型的数组。这是因为内部是通过其他转换器对每个元素进行转换,从而达到整个数组的转换。由于 Spring 没有定义 String
到 Class
的转换器,ArrayToArrayConverter
并不能完成这一任务。
public <T> T convertIfNecessary(Object newValue, Class<T> requiredType, TypeDescriptor typeDescriptor) throws TypeMismatchException {
//1. ConversionService转换(略)
//2. PropertyEditor转换(略)
//3. 特殊的类型转换
if(requiredType != null && convertedValue != null){
if(requiredType.isArray()){
return (T) convertToTypedArray(convertedValue, requiredType.getComponentType());
}
}
return (T) newValue;
}
接下来看 convertToTypedArray
方法的实现,先遍历数组,然后对每个元素进行转换,这里递归调用了 convertIfNecessary
方法。前边提到,Spring 自带的转换器无法完成该任务,别忘了还有属性编辑器,其中有一个是 ClassEditor
,专门用于字符串转 Class
类型。至此,问题得到了解决。
private Object convertToTypedArray(Object input, Class<?> componentType) {
if (input.getClass().isArray()) {
int arrayLength = Array.getLength(input);
Object result = Array.newInstance(componentType, arrayLength);
//遍历数组,对每个元素进行转换
for (int i = 0; i < arrayLength; i++) {
Object value = convertIfNecessary(Array.get(input, i), componentType);
Array.set(result, i, value);
}
return result;
}
return null;
}
总的来说,convertIfNecessary
方法的逻辑并不复杂,将具体的处理委托给转换器和属性编辑器来处理。此外还有一些特殊情况,需要一定的代码,但也就起个调度作用,主要工作还是转换器和属性编辑器完成的。
6. 测试
6.1 转换器
在测试方法中,首先构建 SimpleTypeConverter
对象,然后对几种常见的类型进行转换。比如字符串转数值、字符串转 URL
、字符串和 List
的互转,数组和数组的互转等。Spring 核心包提供了大量转换器,这里仅列举出了一部分,其余类型的转换请读者自行尝试。
//测试方法
@Test
public void testConverter(){
SimpleTypeConverter converter = new SimpleTypeConverter();
int integer = converter.convertIfNecessary("12", int.class);
URL url = converter.convertIfNecessary("https://www.baidu.com", URL.class);
List list = converter.convertIfNecessary("aa,bb,cc", List.class);
String str = converter.convertIfNecessary(Arrays.asList("1", "2", "3"), String.class);
String[] arr = converter.convertIfNecessary(Arrays.asList(4, 5, 6), String[].class);
System.out.println(integer); //字符串转int
System.out.println(url); //字符串转URL
System.out.println(list); //字符串转List
System.out.println(str); //List转字符串
System.out.println(Arrays.toString(arr)); //数组转数组
}
从测试结果来看,所有类型的数据都完成了转换。特别是第 3、4、5 项,转换后的形式发生了改变。
12
https://www.baidu.com
[aa, bb, cc]
1,2,3
[4, 5, 6]
6.2 属性编辑器
上文提到了四个属性编辑器,我们再来看一个有代表性的。InetAddressEditor
的作用是将字符串转换成 InetAddress
对象,Spring Boot 中会用到这个属性编辑器。InetAddress
表示 IP 地址,是一串具有特殊格式的字符串,比如 192.168.0.1
这种。该类不能通过常规的构造器来创建,必须调用指定的静态方法。
//测试类
public class InetAddressEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
return ((InetAddress) getValue()).getHostAddress();
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(InetAddress.getByName(text));
}
}
在测试方法中,先创建 SimpleTypeConverter
实例,然后注册属性编辑器,接下来将字符串类型的 IP 地址转换成 Inet4Address
类型。
//测试方法
@Test
public void testPropertyEditor() {
SimpleTypeConverter converter = new SimpleTypeConverter();
converter.registerCustomEditor(Inet4Address.class, new MyInetAddressEditor());
Inet4Address address = converter.convertIfNecessary("192.168.0.1", Inet4Address.class);
System.out.println(address.getHostAddress());
}
从测试结果来看,输出的仍是 192.168.0.1
,但数据的来源是 Inet4Address
对象,而不是原始的字符串。
192.168.0.1
6.3 复杂转换
测试方法看起来和转换器的测试相同,实际上原理是不同的。上文提到过,Spring 自带的转换器无法处理字符串到 Class
的转换,这里实际上用到了 ClassEditor
。
//测试方法
@Test
public void testOtherConvert(){
SimpleTypeConverter converter = new SimpleTypeConverter();
String[] strArr = new String[] {"java.lang.String", "java.lang.Integer"};
Class[] classArr = converter.convertIfNecessary(strArr, Class[].class);
System.out.println(Arrays.toString(classArr));
}
从测试结果来看,class java.lang.String
正是 Class
的 toString
方法的输出形式,说明这是一个 Class
数组。
[class java.lang.String, class java.lang.Integer]
7. 总结
本节我们讨论了类型转换相关的问题,主要有两种解决方案,一是 JDK 提供的属性编辑器,二是 Spring 核心包提供的转换服务。Spring 通过 TypeConverter
将这两种解决方案整合到一起,再加上对一些特殊情况的处理,构成了强大的类型转换功能。需要说明的是,属性编辑器不是线程安全的,这就导致了 TypeConverter
的功能虽然强大,但不是线程安全的。Spring 设计转换器时考虑到了线程安全的问题,如果对这方面有严格的要求,可以单独使用 ConversionService
。
总的来说,类型转换相关的实现并不复杂,主要还是对已有资源的调配和使用。而这正是面向对象编程的核心理念之一,重复造轮子不是明智的做法,我们要学会合理地调兵遣将。高效的编程实际上是一门管理的学问,凡事不一定都要亲历亲为,面对不同的情况,最大限度地利用已有的资源,多快好省地实现既定目标。
8. 项目信息
本节新增和修改内容一览
beans
├─ src
│ ├─ main
│ │ └─ java
│ │ └─ cn.stimd.spring.beans
│ │ ├─ propertyeditors
│ │ │ ├─ ClassEditor.java (+)
│ │ │ ├─ CustomBooleanEditor.java (+)
│ │ │ ├─ CustomCollectionEditor.java (+)
│ │ │ └─ CustomNumberEditor.java (+)
│ │ ├─ BeansException.java (+)
│ │ ├─ ConversionNotSupportedException.java (+)
│ │ ├─ PropertyAccessException.java (+)
│ │ ├─ PropertyEditorRegistry.java (+)
│ │ ├─ PropertyEditorRegistrySupport.java (+)
│ │ ├─ SimpeTypeConverter.java (+)
│ │ ├─ TypeConverter.java (+)
│ │ ├─ TypeConverterSupport.java (+)
│ │ └─ TypeMismatchException.java (+)
│ └─ test
│ └─ java
│ └─ beans
│ └─ basic
│ ├─ ConvertTest.java (+)
│ └─ InetAddressEditor.java (+)
└─ pom.xml (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,回复「重写SpringFramework」加群一起讨论。
原创不易,觉得内容不错请分享一下。
转载自:https://juejin.cn/post/7370379022285848587