【重写SpringFramework】第一章beans模块:属性访问(chapter 1-3)

1. 前言

上一节我们讨论了类型转换功能是如何实现的,主要有两个用途,一是将对象转换成指定类型,二是在属性访问的过程中进行类型转换。那么什么是属性访问?一般来说,我们可以调用 setter 方法为对象赋值。属性访问则是将一个对象和一组数据关联起来,并自动完成赋值的操作。在赋值的过程中,外部数据可能与对象的字段类型不一致,因此需要进行类型转换。

在这里插入图片描述

属性访问在实际生产中有着广泛的应用,比如 Spring 容器在创建对象的过程中,用户不能直接进行干预,但可以指定一组数据,然后通过属性访问的方式为对象赋值。此外,在 web 应用中,我们可以定义一个对象来自动接收请求参数,也用到了属性访问的功能。更为重要的是,属性访问的强大之处还体现在可以为嵌套属性赋值,这是本节重点讨论的内容。

2. 原理分析

2.1 属性类

在说明属性访问器的具体工作之前,先来看一个实际应用的例子。Spring Boot 加载后缀名为 properties 或 yaml 的配置文件,使用属性类来封装一组相关联的属性,比如 ServerProperties 表示以 server 开头的一组配置项。以下为 properties 格式的配置文件:

server.port=80
server.contextPath=/
server.address=192.168.0.1
server.tomcat.maxThreads=10
server.tomcat.maxConnections=100
server.tomcat.acceptCount=100

yaml 文件的格式略有不同,但从结构上来看更为清晰。

server:
  port: 80
  contextPath: /
  address: 192.168.0.1
  tomcat:
    maxThreads: 10
    maxConnections: 100
    acceptCount: 100

ServerProperties 实际上是一个 Java Bean,四个字段可以分为三种情况。前两种情况 TypeConverter 是可以处理的,关键在于 tomcat 属性,它本身是一个对象,拥有自己的属性,因此属性访问的核心是处理这种嵌套的属性

  • portcontextPath 是简单类型,交由 Converter 接口处理。
  • address 是比较特殊的对象,没有公开的构造器,必须通过静态的 getByName 等工厂方法,因此需要定义一个属性编辑器来访问。
  • tomcat 是内部类,拥有自己的属性,也就是嵌套属性。
//示例代码:属性类
public class ServerProperties {
    private Integer port;
    private String contextPath = "/";
    private InetAddress address;
    private final Tomcat tomcat = new Tomcat();

    public static class Tomcat {
        private int maxThreads = 200;
        private int maxConnections = 10000;
        private int acceptCount = 100;
    }

    //getter、setter方法略
}

2.2 嵌套属性与嵌套对象

我们通过属性类引入了嵌套属性这一概念,本节的目标是实现对嵌套属性的解析。为了更好地表述,需要先厘清两个概念:

  • 嵌套属性:自身是外层类的一个属性,同时该外层类又是另一个类的属性,比如 maxThreads 字段
  • 嵌套对象:自身是外层类的一个属性,同时拥有自己的属性,比如 tomcat 字段
//示例代码:属性类
public class ServerProperties {
    private Integer port;	//普通属性
    private final Tomcat tomcat = new Tomcat();	//嵌套对象

    public static class Tomcat {
        private int maxThreads = 200;	//嵌套属性
    }
}

注:当我们单独讨论 maxThreads 是普通属性还是嵌套属性是没有意义的,必须看它在整个体系中的位置。对于 Tomcat 来说,maxThreads 是普通属性。对于 ServerProperties 来说,maxThreads 是嵌套属性。换句话说,嵌套属性至少有两个外层类,本身则处于第三层或更深层次

2.3 嵌套属性的结构

嵌套属性的组织结构类似于数据结构中的树。最外层对象是根节点,嵌套对象是分支节点,嵌套属性是叶子节点。我们可以得到以下结论:从根节点开始,第一层的叶子节点必然是普通属性,第二层以下的叶子节点必然是嵌套属性,所有的分支节点都是嵌套对象。如果要访问叶子节点,则必须从根节点出发,经过若干层级的分支节点,最后到达叶子节点。在这一过程中,嵌套对象如果不存在,我们需要先创建对象,然后才能访问更深层次的属性。这就涉及到递归的逻辑,源码中将有所体现。

在这里插入图片描述

按照上述结构对 ServerProperties 进行分析,portcontextPathaddress 都是普通属性,tomcat.maxThreadstomcat.maxConnectionstomcat.acceptCount 等是嵌套属性,想要为嵌套属性赋值,就必须先解决嵌套对象 tomcat 的问题。

在这里插入图片描述

3. 属性访问组件

3.1 继承结构

属性访问的类图结构如下所示,绿色部分代表类型转换的相关组件,这说明属性访问是以类型转换为基础的。先来看抽象层次的 API,包括两个接口和一个实现类。

  • PropertyAccessor:顶级接口,定义了获取和设置属性值的方法
  • ConfigurablePropertyAccessor:提供对外配置的接口,继承了 PropertyEditorRegistryTypeConverter 接口,拥有类型转换的能力
  • AbstractPropertyAccessor:属性访问的核心类,实现了属性访问的主要逻辑

在属性访问的具体实现上,出现了两个分支,区别在于访问字段的方式是内省还是反射,具体如下:

  • BeanWrapper:Spring 操作 Java Bean 的核心接口,为 BeanFactoryDataBinder 等组件提供支持。
  • BeanWrapperImpl:实现了 BeanWrapper 接口,通过内省的方式给字段赋值
  • DirectFieldAccessor:通过反射的方式访问字段(实际使用不常见,仅了解)

在这里插入图片描述

3.2 PropertyAccessor

PropertyAccessor 接口定义了属性访问的基本功能,主要是获取和设置属性值的方法。需要注意的是,propertyName 参数可以是普通属性,也可以是嵌套属性。

  • getPropertyValue 方法:获取指定名称的属性值
  • setPropertyValue 方法:为指定名称的属性赋值
  • setPropertyValues 方法:批量设置属性值
public interface PropertyAccessor {
    Object getPropertyValue(String propertyName) throws BeansException;
    void setPropertyValue(String propertyName, Object value) throws BeansException;
    void setPropertyValues(PropertyValues pvs) throws BeansException;
}

3.3 AbstractPropertyAccessor

AbstractPropertyAccessor 继承了 TypeConverterSupport 抽象类,拥有类型转换的能力。为了简化代码,该类还集成了子类 AbstractNestablePropertyAccessor,因此可以对嵌套属性进行访问。该类持有一个 wrappedObject 字段,表示目标对象,属性访问器将对目标对象的属性进行处理。

PropertyHandler 是内部的抽象类,定义了访问属性的行为。子类 BeanWrapperImplDirectFieldAccessor 的内部类继承了PropertyHandler,其中 BeanWrapperImpl 以属性编辑器的方式来处理,而 DirectFieldAccessor 则以反射字段的方式来处理。

public abstract class AbstractPropertyAccessor extends TypeConverterSupport implements ConfigurablePropertyAccessor {
    //目标对象
    Object wrappedObject;

    protected abstract static class PropertyHandler {
        //属性的类型
        private final Class<?> propertyType;

        public abstract Object getValue() throws Exception;
        public abstract void setValue(Object object, Object value) throws Exception;
    }
}

3.4 PropertyValues

属性访问的前提是属性值是已知的,我们还需要了解属性值的来源及其结构。先说来源,除了上文提到的配置文件,也有可能是网络请求的参数,或者是数据库中的数据等。再说结构,属性的 key 和 value 可能是两个独立的变量,也可能是某个实体类,或者是一个 Map。鉴于这两方面的不确定性,Spring 对属性值进行抽象,以统一的方式为对象设置属性。

  • PropertyValues 接口用于描述一组属性值,定义了获取属性值的方法。
public interface PropertyValues {
    PropertyValue[] getPropertyValues();
    PropertyValue getPropertyValue(String propertyName);
}
  • MutablePropertyValues 类实现了 PropertyValues 接口,并持有一个 PropertyValue 的集合。我们可以通过构造方法传入一个 Map,也可以调用 addPropertyValue 方法,添加单个属性值。
public class MutablePropertyValues implements PropertyValues {
    private final List<PropertyValue> propertyValueList;

    public MutablePropertyValues(Map<?, ?> original) {
        this.propertyValueList = new ArrayList<>(original.size());
        for (Map.Entry<?, ?> entry : original.entrySet()) {
            this.propertyValueList.add(new PropertyValue(entry.getKey().toString(), entry.getValue()));
        }
    }

    public void addPropertyValue(String propertyName, Object propertyValue) {
        this.propertyValueList.add(new PropertyValue(propertyName, propertyValue));
    }
}
  • PropertyValue 类表示一个属性,name 表示属性名,value 表示属性值。
public class PropertyValue {
    private final String name;
    private final Object value;
}

5. 嵌套属性的处理

5.1 概述

AbstractPropertyAccessor 类的 setPropertyValue 方法是属性访问的核心方法,参数 propertyName 表示待赋值的属性名。我们以属性类 ServerProperties 的赋值过程进行分析,普通属性比较简单,主要来看嵌套属性 tomcat.maxThreads 是如何处理的。整个流程可以分为两步:

  1. 获取能够处理 tomcat.maxThreads 的属性访问器,实际上是拿到嵌套对象 tomcat 的属性访问器。
  2. 截取最后一个分隔符之后的内容,也就是 maxThreads,并调用 tomcat 的属性访问器对 maxThreads 进行赋值。
/**
 * 为指定的属性赋值
 * @param propertyName 完整的属性名,形式类似b.c
 * @param value        待设置的值
 */
public void setPropertyValue(String propertyName, Object value) throws BeansException {
    //1. 获取嵌套对象的访问器
    AbstractPropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);

    //2. 为(嵌套)属性赋值
    //如果propertyName是b.c,那么nestPropertyName就是c
    String nestPropertyName = propertyName.substring(propertyName.lastIndexOf('.') + 1);
    nestedPa.processLocalProperty(new PropertyValue(nestPropertyName, value));
}

5.2 获取属性访问器

对于嵌套属性 tomcat.maxThreads 来说,getPropertyAccessorForPropertyPath 方法将调用两次。第一次调用进入分支一,尝试获取属性名的第一个分隔符 . 的下标,大于 1 说明下标存在,这样做是为了判断入参 propertyPath 是否为嵌套属性。接下来的操作分为三步:

  1. 将属性名分割为两部分,nestedProperty 变量的值是 tomcatnestedPath 变量的值是 maxThreads
  2. 调用 getNestedPropertyAccessor 方法获取 tomcat 的属性访问器,详情见下文。
  3. 再次调用 getPropertyAccessorForPropertyPath 方法,获取能够处理 maxThreads 的属性处理器。

第二次调用 getPropertyAccessorForPropertyPath 方法,此时 propertyPath 参数为 maxThreads,不是嵌套属性,进入分支二,直接返回 this 即可。注意,此时 this 指向的是嵌套属性 tomcat 对应的属性访问器。

private AbstractPropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
    //case-1 获取属性名的第一个.的下标
    int pos = propertyPath.indexOf('.');
    if(pos > -1){
        //如果propertyPath是b.c,则pos为1,nestedProperty为b,nestedPath为c
        String nestedProperty = propertyPath.substring(0, pos);
        String nestedPath = propertyPath.substring(pos + 1);

        //对于b.c来说,b被看作一个嵌套对象,需要创建对应的属性访问器
        AbstractPropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
        return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
    }
    //case-2 普通属性直接返回当前PropertyAccessor
    return this;
}

对于嵌套属性来说,继续执行 getNestedPropertyAccessor 方法,此时方法入参 nestedProperty 的值为 tomcatgetNestedPropertyAccessor 方法的作用是为 tomcat 创建一个属性访问器,可以分为三步:

  1. 获取 PropertyHandler 的实例,我们不必关心 getLocalPropertyHandler 方法的实现细节,PropertyHandler 就是用来访问属性的工具,无非是内省和反射的区别罢了。
  2. 检查 tomcat 的属性值是否存在,如果为空则创建一个 Tomcat 实例,赋给 ServerProperties 对象的 tomcat 字段。注意,processLocalProperty 方法解决的是普通属性的赋值问题,后续流程还会调用,此处先不展开。
  3. 为嵌套对象创建一个属性访问器并返回。newPropertyAccessor 方法是由子类完成的,比如 BeanWrapperImpl 的实现是创建了一个新的 BeanWrapperImpl 对象。

注:在为 tomcat.maxThreadstomcat.maxConnectionstomcat.acceptCount 等嵌套属性赋值时,公共前缀 tomcat 对应的 Tomcat 对象是同一个。也就是说,Tomcat 对象会被缓存到 PropertyHandler 实例中。这一点也很好理解,如果每次返回不同的 Tomcat 对象,最终得到的对象只有一个属性被赋值。

private AbstractPropertyAccessor getNestedPropertyAccessor(String nestedProperty) {
    //1. 获取PropertyHandler
    PropertyHandler ph = getLocalPropertyHandler(nestedProperty);
    if (ph == null || !ph.isReadable()) {
        throw new NotReadablePropertyException(wrappedObject.getClass().getName(), nestedProperty);
    }

    //2. 确保嵌套对象是存在的
    Object value = ph.getValue();
    if(value == null){
        //嵌套属性可能为null,需要创建一个实例(数组、Collection、Map类型略)
        value = BeanUtils.instantiateClass(ph.getPropertyType());
        //将嵌套对象的实例赋给当前对象
        processLocalProperty(new PropertyValue(nestedProperty, value));
    }
    //3. 为嵌套对象创建一个属性访问器(由子类实现)
    return newPropertyAccessor(value);
}

5.3 属性赋值

我们将视角转回最开始的 setPropertyValue 方法,在得到了嵌套对象的属性访问器之后,接下来就是为属性赋值。到了这一步已经不存在嵌套属性的概念了,因为嵌套属性已经被分解成一个普通对象 tomcat 和一个普通属性 maxThreads。也就是说,我们已经把 tomcat 赋给 server 了, 现在要做的是把 maxThreads 赋给 tomcat。搞明白了这一点,processLocalProperty 方法的作用就显而易见了,专门负责为普通属性赋值(之前调用过一次,但没有展开讲)。processLocalProperty 方法可以分为三步:

  1. 获取属性处理器,这里需要判断是否可写,实际上是检查 setter 方法是否存在
  2. 获取属性值,进行必要的类型转换
  3. 为当前对象的属性赋值,具体取决于子类实现,通过反射或内省的方式赋值(我们只关心 BeanWrapperImpl ,代码比较简单,不赘述)
private void processLocalProperty(PropertyValue pv) {
    //1. 获取属性处理器
    PropertyHandler ph = getLocalPropertyHandler(pv.getName());
    if (ph == null || !ph.isWritable()) {
        throw new NotWritablePropertyException("settter方法不存在");
    }

    //2. 进行必要的类型转换
    Object valueToApply = pv.getValue();
    valueToApply = convertIfNecessary(valueToApply, ph.getPropertyType());

    //3. 为当前对象的属性赋值
    ph.setValue(getWrappedInstance(), valueToApply);
}

5.4 执行流程分析

setPropertyValue 方法涉及到递归调用,单从代码来看不是很直观,我们结合流程图重新梳理一下整个流程。还是以 tomcat.maxThreads 为例,观察嵌套属性是如何赋值的,整个过程可以分为五步:

  1. 检查 tomcat.maxThreads 存在分隔符,得出 tomcat 是嵌套对象,需要获取相应的属性访问器。
  2. 检查 server 对象的 tomcat 属性是否存在,首次查询肯定是不存在的,需要创建 Tomcat 的实例,然后为 server 对象的 tomcat 属性赋值。
  3. 返回 tomcat 的属性访问器,此时程序不知道 tomcat.maxThreads 的第一个分隔符之后的字符串是否还有分隔符 ,因此递归调用 getPropertyAccessorForPropertyPath 方法。
  4. 此时 maxThreads 不存在分隔符,因此直接返回 this,也就是 tomcat 的属性访问器。
  5. 回到 processLocalProperty 方法,使用 tomcat 的属性访问器为 maxThreads 赋值。

在这里插入图片描述

整个过程中有两次赋值操作,第一次发生在 step-2,创建 Tomcat 对象并为 ServerProperties 对象的 tomcat 字段赋值。第二次发生在 step-5,此时的属性值是 setPropertyValue 方法的入参,为 Tomcat 对象的 maxThreads 字段赋值。同样地,如果继续为 tomcat.maxConnectionstomcat.acceptCount 赋值,均只触发一次赋值操作,这是因为 tomcat 字段不为 null,直接为嵌套属性赋值即可。

6. 测试

测试方法分为三步。首先创建 BeanWrapperImpl 的实例来包装 ServerProperties 对象,实际上是为外层对象创建了一个属性访问器。由于需要解析 InetAddress 类型的属性,需要注册自定义的属性编辑器 InetAddressEditor。其次,配置文件的参数使用 Map 来模拟,并使用 MutablePropertyValues 进行包装。最后,调用 setPropertyValues 方法,将所有的属性赋给目标对象。

@Test
public void testPropertyAccessor(){
    ServerProperties properties = new ServerProperties();
    BeanWrapper wrapper = new BeanWrapperImpl(properties);
    wrapper.registerCustomEditor(InetAddress.class, new InetAddressEditor());  //注册属性编辑器
    wrapper.setConversionService(new DefaultConversionService());               //设置转换服务

    //模拟配置信息
    Map<String, Object> props = new HashMap<>();
    props.put("port", 80);
    props.put("contextPath", "/spring");
    props.put("address", "192.168.0.101");
    props.put("tomcat.maxThreads", "10");
    props.put("tomcat.maxConnections", "20");
    props.put("tomcat.acceptCount", "5");
    wrapper.setPropertyValues(new MutablePropertyValues(props));

    System.out.println("属性访问测试: " + wrapper.getWrappedInstance());
    System.out.println("获取嵌套属性的值:" + wrapper.getPropertyValue("tomcat.maxThreads"));
}

从测试结果可以看到,ServerProperties 对象的普通属性和嵌套属性都被赋值。同样地,我们也可以获取嵌套属性的值。

属性访问测试: ServerProperties{port=80, contextPath=/spring, address=/192.168.0.101, tomcat=Tomcat{maxThreads=10, maxConnections=20, acceptCount=5}}
获取嵌套属性的值:10

7. 总结

本节我们介绍了什么是属性访问,针对一些复杂对象,引入了嵌套属性和嵌套对象的概念,并指出复杂对象的结构实际上是一颗树。为了访问最深层次的属性,我们从根节点出发,经过若干分支节点,最终抵达叶子节点。从某种程度上来说,属性访问可以看做是树的深度遍历(只说近似是因为有一定的条件限制,必须像测试代码那样保证属性值的顺序)。

属性访问的核心逻辑就是对复杂对象进行分解,最终得到一个二元结构的简单对象。比如 a-b-c 的关系,先提取出 a-b 来,解决掉 b 的问题。然后再提取出 b-c,解决掉 c 的问题。在分解的过程中,我们需要注意的是相对视角的转换。从总体上看,b 是嵌套对象,c 是嵌套属性,但在 a-b 的分解中,b 是 a 的普通属性;在 b-c 的分解中,c 是 b 的普通属性。化繁为简,寻找可能存在的共性,并使用统一的逻辑进行处理。这是属性访问带给我们的启示,Spring 也经常使用这种方法论来处理复杂问题。

8. 项目信息

本节新增和修改内容一览

beans
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring.beans
   │        ├─ AbstractPropertyAccessor.java (+)
   │        ├─ BeanUtils.java (+)
   │        ├─ BeanWrapper.java (+)
   │        ├─ BeanWrapperImpl.java (+)
   │        ├─ ConfigurablePropertyAccessor.java (+)
   │        ├─ InvalidPropertyException.java (+)
   │        ├─ MutablePropertyValues.java (+)
   │        ├─ NotReadablePropertyException.java (+)
   │        ├─ NotWritablePropertyException.java (+)
   │        ├─ NullValueInNestedPathException.java (+)
   │        ├─ PropertyAccessor.java (+)
   │        ├─ PropertyValue.java (+)
   │        └─ PropertyValues.java (+)
   └─ test
      └─ java
         └─ beans
            └─ basic
               ├─ ConvertTest.java (*)
               └─ ServerProperties.java (+)

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

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

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


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

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值