Spring解析之IoC:XML配置文件的加载及BeanFactory的创建

前言
本文是Spring源码解析IoC部分的第一篇文章,以最简单的IoC案例作为切入点,主要分析了XML配置文件是如何被加载的,Bean工厂是如何创建的

为了分析方便,本文创建了一个普通java类Student,有String类型nameint类型的age两个属性,此外还有一个公共无参,无返回值的void study()方法,初学时不管有多少种加载配置文件的方法new ClassPathXmlApplicationContext(String)必然是最常用的一种。我们在resources目录下创建了名为beans.xml的Spring配置文件

图1. 初始化beans.xml配置

在配置文件中使用<bean>表明将Student交给Spring来管理,这里需要插一嘴,IoC最主要的作用是将繁琐的创建对象的过程与代码需要真正表达的业务逻辑解耦,所谓的创建对象过程还要分为两部分:1.构建出对象实体;2.建立对象间依赖关系,其中第二部分主要通过依赖注入DI实现,所以说很多书上会将DI作为IoC实现的手段。完成配置后我们就可以通过一段简单的代码得到托管的Student,并调用其中的study()
图2. 从Spring容器中得到托管对象

既然第二句已经获取被托管的对象,那么IoC的主流程必然存在于第一句中,我们就以new ClassPathXmlApplicationContext(String)为入口,看看Spring是如何完成控制反转的
图3. ClassPathXmlApplicationContext构造器

图中最后一个构造器才是逻辑真正的开始位置,第一个参数为配置文件数组,第二个参数表示是否需要刷新Spring上下文标识,这里为true,第三个参数为当前context的父上下文。在进一步深入之前我需要先给出ClassPathXmlApplicationContext的类图,类图就像指南针,当我们深入细节迷失时会引导我们识别正确的道路
图4. ClassPathXmlApplicationContext类图

图3中setConfigLocations(configLocations)主要作用是解析ClassPathXmlApplicationContext(String)参数中的配置文件路径,并将配置的单个或者多个配置文件存放在父类AbstractRefreshableConfigApplicationContext的成员变量configLocations数组内。如果路径中存在${}占位符,会用正确的值进行替换,由于其处理过程和使用<context:property-placeholder>引入外部配置文件处理流程一致,所有将在后面解析<context:property-placeholder>原理时单独讲解。设置完配置文件后调用refresh()刷新整个Spring上下文信息
图5. AbstractApplicationContext的refresh()

该方法包裹在同步代码块中,防止整个Spring上下文刷新时多个线程造成的冲突,在查看对象监视器startupShutdownMonitor的使用时发现,在同类的close()中也使用了该对象监视器的同步代码块,这也引出了另一层含义,刷新Spring上下文相当于“创建”,而close()相当于”销毁”,共享同一个对象监视器保证了对Spring上下文的两种“侵入性”操作不能共存。此外该方法在AbstractApplicationContext中,很明显的模板方法设计模式,prepareRefresh()进行刷新前的准备工作
图6. AbstractApplicationContext的prepareRefresh()

方法中initPropertySources()为空实现不用理会,getEnvironment()得到一个”标准环境对象”StandardEnvironment,而validateRequiredProperties()负责校验加载时配置文件或者系统参数中存在必须配置但没有配置的情况,所有必须配置的属性存储在AbstractPropertyResolver中的成员变量Set<String> requiredProperties中,如果检测到需要配置而未配置的属性抛出MissingRequiredPropertiesException。接着看图5中obtainFreshBeanFactory()
图7. AbstractApplicationContext的obtainFreshBeanFactory()

该方法顾名思义就是获得新的BeanFactory,总体实现思路超级简单:1.创建BeanFactory,由refreshBeanFactory()完成;2.取出新的BeanFactory返回,由getBeanFactory()完成。先看看第一步
图8. AbstractRefreshableApplicationContext的refreshBeanFactory()

首先根据hasBeanFactory()判断成员变量beanFactory是否为null,该方法由beanFactoryMonitor对象监视器的同步块包裹,如果已经存在bean工厂先进行销毁,之后调用createBeanFactory()创建新的工厂,该方法中newBeanFactory接口的子类DefaultListableBeanFactory,该类是bean工厂的对外暴露的核心类,这里同样给出该类的类图,供读者在源码中迷失时指引方向
图9. DefaultListableBeanFactory的类图

我们从图中发现DefaultListableBeanFactory不仅实现了BeanFactory接口,同时也实现了BeanDefinitionRegistry,在开篇中我们曾经说过,对象存放在BeanFactory中,而“放”这个动作由BeanRegistry完成,现在两者统一合成到了一个类中。这就像刚学Java面向对象思想的时候老师举过的一个例子,人去关灯这件事,如果要用面向对象的思想表示,”关灯”这个动作应该是封装在灯对象中而不是人对象,随着源码分析的不断深入,我们还会进一步剖析DefaultListableBeanFactory
创建完后给bean工厂设置唯一标识,该标识和实现Serializable后生成的serialVersionUID作用相似,都是保证序列化和反序列化后的对象一致性。
图10. AbstractRefreshableApplicationContext的customizeBeanFactory(DefaultListableBeanFactory)

该方法用于设置BeanFactory的自定义属性,其实里面就两个:allowBeanDefinitionOverriding,当多个配置文件中存在同一个id或者name标识的标签时是否用后加载配置文件中的内容覆盖先加载的,我们可以在创建ClassPathXmlApplicationContext后自主设置,注释上说设为false就会发生覆盖,设为true当出现重复时会抛出异常,但恕我愚笨,无论怎么模拟都只发生了覆盖但不抛出异常,希望读者有知道的给我说说如何模拟;allowCircularReferences表示是否允许循环依赖,说到这个循环依赖我可是吃过它的亏。在第一家公司工作的时候项目组织的很混乱,Service层有时依赖Dao,有时又图省事Service之间相互依赖,时不时出现循环依赖问题,造成项目无法启动,关于这种问题如何解决我分享给大家一篇文章Circular Dependencies in Spring,里面提供了多种解决思路,这里不再赘述。最后一句代码也是非常重要的,看着名字大家十有八九都能猜到它的功能与@Autowired@Qualifier相关。在Spring中特别喜欢用xxxResolverxxxHandler给类命名,表示对某个东西的处理类,注解的实现和运行原理也会专门开一篇详述,这里不往下深入。回到图8loadBeanDefinitions(DefaultListableBeanFactory)封装了读取配置文件并解析的流程
图11. AbstractXmlApplicationContext的loadBeanDefinitions(DefaultListableBeanFactory)

该类的具体实现在父类AbstractXmlApplicationContext中(如果迷失请看类图),第一句创建了XmlBeanDefinitionReader的实例,和开篇我们推测的一样,主要功能是将配置文件读取到内存中,之后为其设置“当前运行环境”,其实就是上面设置过的StandardEnvironmentResourceEntityResolver代表配置文件的解析器,xml的语法定义约束有两种:DTD和XSD,在该解析器的父类DelegatingEntityResolver中就分别定义了两种特定类型的解析器
图12. DelegatingEntityResolver构造器

此外在初始化PluggableSchemaResolver中会加载默认schemas,位置在每个Spring模块的META-INF/spring.schemas,比如对于3.2.8.RELEASEspring-beans模块,默认的schemas如下
图13. 3.2.8.RELEASE版本spring-beans模块的schemas

initBeanDefinitionReader(XmlBeanDefinitionReader)内设置默认解析xml配置文件需要验证,流程走到loadBeanDefinitions(XmlBeanDefinitionReader)
图14. AbstractXmlApplicationContext的loadBeanDefinition(XmlBeanDefinitionReader)

按照本文的例子启动Spring容器时configResources为空,所有的配置文件都存放在configLoactions数组中,当然我们也可以启动Spring容器的时候不传入配置文件,如果不传入的话getConfigLoactions()会调用子类XmlWebApplicationContextgetDefaultConfigLocations()返回默认位置/WEB-INF/applicationContext.xml的配置文件。随后用参数的reader加载配置文件
图15. AbstractBeanDefinitionReader的loadBeanDefinitions(String, Set<Resource>)

流程走到XmlBeanDefinitionReader的父类AbstractBeanDefinitionReader的上图中方法,第一个参数是每一个配置文件,第二个参数为null。这里的resourceLoader就是ClassPathXmlApplicationContext的实例,因为该实例实现了ResourceLoader接口,具体的赋值过程在图11中,流程进入红框处代码,省略中间很多非重点调用,最终创建BeanDefinitionDocumentReader实例解析xml文件并进行BeanDefinition的注册
图16. DefaultBeanDefinitionDocumentReader解析xml并注册BeanDefinition入口

标注1是进来的第一步,Document是Spring使用SAX解析xml之后得到的Document对象,然后获取xml的根元素对象并传入标注2方法,首先处理Document中的profile属性,使用该属性的人可能不多,他就像maven中的<profiles><profile></profile></profiles>组合,可以根据运行的不同环境加载不同profile的值。之后创建一个委派类BeanDefinitionParserDelegate进行xml文件解析的初始化,其中主要对顶层<beans>内的属性进行解析,在BeanDefinitionParserDelegate中存储了很多常量,每一个常量都与xml中一个标签或者属性对应,其中就有一组<beans>中的属性
图17. BeanDefinitionParserDelegate中与<beans>属性对应的常量

属性具体的用法请读者翻阅Spring in Action或者其他资料,这里只分析流程不做详细解释,回到图16。preProcessor(Element)postProcessXml(Element)分别用户子类继承重写对xml解析进行前置处理、后置处理。parseBeanDefinitions(Element, BeanDefinitionParserDelegate)对我们常用的,诸如<bean><context:property-placeholder>进行解析

后记
为了突出主要流程,在文中留了很多可继续深挖的扩展点,这些扩展点后期会用“外传”的形式另起文章分析。随着源码阅读的深入,对之前文章中知识点的理解必定也会有所不同,所以之后会随时对Spring相关文章进行修改更新