【重写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
属性,它本身是一个对象,拥有自己的属性,因此属性访问的核心是处理这种嵌套的属性。
port
和contextPath
是简单类型,交由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
进行分析,port
、contextPath
、address
都是普通属性,tomcat.maxThreads
、tomcat.maxConnections
、tomcat.acceptCount
等是嵌套属性,想要为嵌套属性赋值,就必须先解决嵌套对象 tomcat
的问题。
3. 属性访问组件
3.1 继承结构
属性访问的类图结构如下所示,绿色部分代表类型转换的相关组件,这说明属性访问是以类型转换为基础的。先来看抽象层次的 API,包括两个接口和一个实现类。
PropertyAccessor
:顶级接口,定义了获取和设置属性值的方法ConfigurablePropertyAccessor
:提供对外配置的接口,继承了PropertyEditorRegistry
和TypeConverter
接口,拥有类型转换的能力AbstractPropertyAccessor
:属性访问的核心类,实现了属性访问的主要逻辑
在属性访问的具体实现上,出现了两个分支,区别在于访问字段的方式是内省还是反射,具体如下:
BeanWrapper
:Spring 操作 Java Bean 的核心接口,为BeanFactory
和DataBinder
等组件提供支持。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
是内部的抽象类,定义了访问属性的行为。子类 BeanWrapperImpl
和 DirectFieldAccessor
的内部类继承了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
是如何处理的。整个流程可以分为两步:
- 获取能够处理
tomcat.maxThreads
的属性访问器,实际上是拿到嵌套对象tomcat
的属性访问器。 - 截取最后一个分隔符之后的内容,也就是
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
是否为嵌套属性。接下来的操作分为三步:
- 将属性名分割为两部分,
nestedProperty
变量的值是tomcat
,nestedPath
变量的值是maxThreads
。 - 调用
getNestedPropertyAccessor
方法获取tomcat
的属性访问器,详情见下文。 - 再次调用
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
的值为 tomcat
。getNestedPropertyAccessor
方法的作用是为 tomcat
创建一个属性访问器,可以分为三步:
- 获取
PropertyHandler
的实例,我们不必关心getLocalPropertyHandler
方法的实现细节,PropertyHandler
就是用来访问属性的工具,无非是内省和反射的区别罢了。 - 检查
tomcat
的属性值是否存在,如果为空则创建一个Tomcat
实例,赋给ServerProperties
对象的tomcat
字段。注意,processLocalProperty
方法解决的是普通属性的赋值问题,后续流程还会调用,此处先不展开。 - 为嵌套对象创建一个属性访问器并返回。
newPropertyAccessor
方法是由子类完成的,比如BeanWrapperImpl
的实现是创建了一个新的BeanWrapperImpl
对象。
注:在为
tomcat.maxThreads
、tomcat.maxConnections
、tomcat.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
方法可以分为三步:
- 获取属性处理器,这里需要判断是否可写,实际上是检查 setter 方法是否存在
- 获取属性值,进行必要的类型转换
- 为当前对象的属性赋值,具体取决于子类实现,通过反射或内省的方式赋值(我们只关心
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
为例,观察嵌套属性是如何赋值的,整个过程可以分为五步:
- 检查
tomcat.maxThreads
存在分隔符,得出tomcat
是嵌套对象,需要获取相应的属性访问器。 - 检查
server
对象的tomcat
属性是否存在,首次查询肯定是不存在的,需要创建Tomcat
的实例,然后为server
对象的tomcat
属性赋值。 - 返回
tomcat
的属性访问器,此时程序不知道tomcat.maxThreads
的第一个分隔符之后的字符串是否还有分隔符 ,因此递归调用getPropertyAccessorForPropertyPath
方法。 - 此时
maxThreads
不存在分隔符,因此直接返回this
,也就是tomcat
的属性访问器。 - 回到
processLocalProperty
方法,使用tomcat
的属性访问器为maxThreads
赋值。
整个过程中有两次赋值操作,第一次发生在 step-2,创建 Tomcat
对象并为 ServerProperties
对象的 tomcat
字段赋值。第二次发生在 step-5,此时的属性值是 setPropertyValue
方法的入参,为 Tomcat
对象的 maxThreads
字段赋值。同样地,如果继续为 tomcat.maxConnections
和 tomcat.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 (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,回复「重写SpringFramework」加群一起讨论。
原创不易,觉得内容不错请分享一下。
转载自:https://juejin.cn/post/7371357451751751732