【重写SpringFramework】第一章beans模块:类型转换(chapter 1-2)

228 阅读14分钟

1. 前言

BeanFactory 是和 Bean 打交道的,Bean 实际上就是一个对象。既然是对象,那么就可能涉及到类型转换的问题。同时,还需要为对象的属性赋值,而赋值的过程也会使用类型转换。因此我们需要先了解类型转换和属性访问这两个基本功能,其中属性访问是以类型转换为基础的。本节我们先讨论类型转换功能,初步了解 Spring 是如何组织代码的。

2. 整体结构

我们在分析一个功能时,先来看它的整体结构,只有养成看类图的习惯,才能建立起全局意识。类型转换的继承体系可以分为三组,一是 JDK 提供的属性编辑器,二是 Spring 核心包提供的转换服务,它们都是完成类型转换工作的具体组件,下面会详细说明。第三组是本节需要实现的 API,使用蓝色标识,简单介绍如下:

  • TypeConverter:顶级接口,定义了类型转换的相关方法
  • TypeConverterSupport:类型转换的核心类,几乎所有转换逻辑都由该类实现
  • SimpleTypeConverter:简单实现类,使用 DefaultConversionService 作为转换服务的实例
  • PropertyEditorRegistry:定义了注册和查找自定义的属性编辑器的方法
  • PropertyEditorRegistrySupport:持有一组属性编辑器,除了 Spring 默认的属性编辑器之外,用户可以添加自定义的属性编辑器

2.1 类型转换类图.png

注:类图与源码的结构有一定的区别。比如,源码中 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 提供了大量的转换器,它们是 ConverterConditionalConverterGenericConverter 等接口的实现类。下图为 Converter 接口的主要实现类。

2.2 Converter主要实现类.png

转换器类负责基本类型、时间日期、字符集、数组、集合、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:允许对某个属性进行编辑,当属性是一个对象时,赋值和取值的过程中完成了类型转换

2.3 内省类图.png

我们主要关心属性编辑器的实现。PropertyEditor 接口定义了一组访问属性的方法,子类 PropertyEditorSupport 主要实现了 setValuegetValue 方法。自定义的属性编辑器需要继承 PropertyEditorSupport,并重写 setAsTextgetAsText 方法。

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 目录下。

2.4 默认的属性编辑器.png

这些类比较简单,以 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 方法

第一步,尝试使用转换器来处理。首先检查是否支持从源类型到目标类型的转换,如果支持则调用 ConversionServiceconvert 方法处理。需要注意的是,如果自定义的属性编辑器存在,那么跳过转换器的处理,直接进入第二步。

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);
        }
    }
}

第二步,尝试使用属性编辑器处理。优先使用自定义的属性编辑器,如果没找到则查找默认的属性编辑器。如果属性编辑器存在,则调用 setAsTextsetValue 方法赋值。这时属性编辑器内部已经创建了目标类型的实例,还需要调用 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 没有定义 StringClass 的转换器,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 正是 ClasstoString 方法的输出形式,说明这是一个 Class 数组。

[class java.lang.String, class java.lang.Integer]

7. 总结

本节我们讨论了类型转换相关的问题,主要有两种解决方案,一是 JDK 提供的属性编辑器,二是 Spring 核心包提供的转换服务。Spring 通过 TypeConverter 将这两种解决方案整合到一起,再加上对一些特殊情况的处理,构成了强大的类型转换功能。需要说明的是,属性编辑器不是线程安全的,这就导致了 TypeConverter 的功能虽然强大,但不是线程安全的。Spring 设计转换器时考虑到了线程安全的问题,如果对这方面有严格的要求,可以单独使用 ConversionService

2.5 类型转换示意图.png

总的来说,类型转换相关的实现并不复杂,主要还是对已有资源的调配和使用。而这正是面向对象编程的核心理念之一,重复造轮子不是明智的做法,我们要学会合理地调兵遣将。高效的编程实际上是一门管理的学问,凡事不一定都要亲历亲为,面对不同的情况,最大限度地利用已有的资源,多快好省地实现既定目标。

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」加群一起讨论。

原创不易,觉得内容不错请分享一下。