以Spring的XML配置文件为例,日常工作中现有applicationContext.xml 然后在文件中添加标签配置。显然就BeanDefinition加载而言,需要对Document(文档)和Element(元素)两种颗粒度的处理。特别地,我们的一个应用可能包含多个配置文件,多个配置文件包含了整个ApplicationContext中的bean。显然,这是一个更大的颗粒度,也就是容器级。从元素,文档到容器,我认为这是一次系统颗粒度的划分和识别,更抽象来说,这就是颗粒度问题。那为什么需要注意颗粒度问题?
不同颗粒级别就是对业务的一种拆分,拆分就意味着可能重用。毕竟软件工程的两大原则就是分解和重用。所以,良好的颗粒度识别和划分是系统设计或者架构的第一步。如果有看过UI/UE去设计网页,首先要做的就是页面布局,也就是页面上该划分几块,每块的内容是什么。如果用数学描述,这算是某种程度上的离散化。
因此,合理的颗粒度划分非常关键。Spring对BeanDefinition的加载处理就是按照这种颗粒度来组织加载逻辑的。
想容纳所有的BeanDefinition,我们需要一个容器,这个容器提供方法,向其中加入/删除BeanDefinition,这就是BeanDefinitionRegistry,后续讨论中我们认为BeanDefinitionRegistry就是我们的BeanDefinition容器。从颗粒度的角度,这里是容器级。
BeanDefinitionRegistry中对应的方法是:
// 注册(添加)BeanDefinition
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException;
// 删除BeanDefinition
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
仅仅拥有容器是不够的,因为没有实体调用registerBeanDefinition方法,去添加BeanDefinition。所以需要一个调用者,在Spring中就是BeanDefinitionReader,对于XML文件配置的应用来说,具体实现类为XmlBeanDefinitionReader. 该类掌控全局完成所有的BeanDefinition加载。之所以强调全局,是因为这里要放全局也就是跨XML文件级别的逻辑。具体有哪些呢?
具体到单个XML文件的处理,则是由BeanDefinitionDocumentReader来完成,具体实现是DefaultBeanDefinitionDocumentReader。这个类有3点需要注意:
protected void preProcessXml(Element root) {}
protected void postProcessXml(Element root) {}
通过XmlReaderContext接收全局级别对象
该类中并未持有对XmlBeanDefinitionReader的引用,而是通过XmlReaderContext间接访问到BeanDefinitionReader中的全局对象。如果是我来实现肯定在初始化DefaultBeanDefinitionDocumentReader时,把 BeanDefinitionReader 的this带过去。但是从OOP的角度,“局部"作为"下层"没必要知道自己在哪个"全局”(上层)中,所以直接带this是不合适的。既然没必要知道,那连Context也没必要要,为什么要加呢?后面再聊。
元素级别的上下文关联
对于嵌套的Beans标签,在处理完成后,处理上下文需要恢复到包含Beans的上级标签。这似乎符合栈数据结构的使用场景,FILO。不过Spring不是这么做的,由于只有两级,直接使用parent记录父级上下文,Beans标签处理完成后,将当前处理上下文恢复。仔细想来,3个层级以内的,这么搞应该都是可以的。
BeanDefinitionDocumentReader的核心方法:
protected void doRegisterBeanDefinitions(Element root) {
// parent 缓存 当前delegete,然后当前delegate 更新为子级别的delegate
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
// 预留的扩展方法
preProcessXml(root);
// 完成具体解析的方法
parseBeanDefinitions(root, this.delegate);
// 预留的扩展方法
postProcessXml(root);
this.delegate = parent;
}
到了标签级别,除了自定义标签,主要有4大类,import, bean, beans 和alias. 这里除了import外,其他的标签都可以在当前上下文中解析。为啥import不行呢?
因为import指向的是另外一个XML文件,显然这里需要文件级别的对象提供支持,但是偏偏被标签级别的解析器处理。小马拉大车显然不行,考虑到逻辑重用,Spring得去重用文档级别上的处理逻辑,但是怎么比较符合OOP的方式调用到对应的方法是个问题。Spring是通过ReaderContext解决的,此处也解释了为什么需要1?
类似的也可以考虑ApplicationContext,连接BeanFactory和程序对外的一些扩展功能。
对系统划分颗粒之后,还要把这种颗粒给粘合起来,否则就是一盘散沙。如何把颗粒之间的粘合起来,就是要确定颗粒之间的关联关系。也就是两个颗粒之间什么关系,是否需要关联的,该以何种方式关联。结合个人对Spring源码的阅读来聊聊这个问题。
将不同颗粒度的对象的关联为一个整体,就像树结构一样,从树根开始长出树枝,树枝上长出树叶。抽象到数据结构中,树枝是树根一级子节点,树叶则是树根的二级子节点,当然更复杂的可能有更多级。显然,每一级在逻辑上对应更小的颗粒度,上一层级关联下一层级,直到最小的不可再分割的颗粒度。
在实际工程中,关联有单双向区分。而类似BeanDefinition解析过程,需要下层访问上层,意味着双向访问。此外,有了双向访问后,同一个树根下的子节点可以借助树根调用到兄弟节点的逻辑。最终可能是,任何层级节点,可以调用其他层级的节点,这种自由度,可能是单纯的树结构无法做到的,当然也会更加复杂。
以上就是今天要聊的内容,从颗粒化的作用,Spring中颗粒度的划分,层级之间双向关联的处理,最后到回归系统整体角度,看对不同层级颗粒对象的粘合。一方面对Spring BeanDefinition 的解析过程有一个清晰的脉络,也对设计模式的应用提供参考案例。