【重写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 提供了大量的转换器,它们是 ConverterConditionalConverterGenericConverter 等接口的实现类。下图为 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 主要实现了 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 目录下。

在这里插入图片描述

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

在这里插入图片描述

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

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 (+)

注:+号表示新增、*表示修改
  • 项目地址:https://gitee.com/stimd/spring-wheel

  • 本节分支:https://gitee.com/stimd/spring-wheel/tree/chapter1-2

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。
在这里插入图片描述

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值