Spring解析之IoC:XML配置文件的加载及BeanFact
2018-01-18 本文已影响193人
宝之家
前言
本文是Spring源码解析IoC部分的第一篇文章,以最简单的IoC案例作为切入点,主要分析了XML配置文件是如何被加载的,Bean工厂是如何创建的
为了分析方便,本文创建了一个普通java类Student
,有String
类型name
和int
类型的age
两个属性,此外还有一个公共无参,无返回值的void study()
方法,初学时不管有多少种加载配置文件的方法new ClassPathXmlApplicationContext(String)
必然是最常用的一种。我们在resources
目录下创建了名为beans.xml
的Spring配置文件
在配置文件中使用
<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()
创建新的工厂,该方法中new
了BeanFactory
接口的子类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中特别喜欢用xxxResolver
、xxxHandler
给类命名,表示对某个东西的处理类,注解的实现和运行原理也会专门开一篇详述,这里不往下深入。回到图8loadBeanDefinitions(DefaultListableBeanFactory)
封装了读取配置文件并解析的流程图11. AbstractXmlApplicationContext的loadBeanDefinitions(DefaultListableBeanFactory)
该类的具体实现在父类
AbstractXmlApplicationContext
中(如果迷失请看类图),第一句创建了XmlBeanDefinitionReader
的实例,和开篇我们推测的一样,主要功能是将配置文件读取到内存中,之后为其设置“当前运行环境”,其实就是上面设置过的StandardEnvironment
。ResourceEntityResolver
代表配置文件的解析器,xml的语法定义约束有两种:DTD和XSD,在该解析器的父类DelegatingEntityResolver
中就分别定义了两种特定类型的解析器图12. DelegatingEntityResolver构造器
此外在初始化
PluggableSchemaResolver
中会加载默认schemas
,位置在每个Spring模块的META-INF/spring.schemas
,比如对于3.2.8.RELEASE
的spring-beans
模块,默认的schemas
如下图13. 3.2.8.RELEASE版本spring-beans模块的schemas
initBeanDefinitionReader(XmlBeanDefinitionReader)
内设置默认解析xml配置文件需要验证,流程走到loadBeanDefinitions(XmlBeanDefinitionReader)
图14. AbstractXmlApplicationContext的loadBeanDefinition(XmlBeanDefinitionReader)
按照本文的例子启动Spring容器时
configResources
为空,所有的配置文件都存放在configLoactions
数组中,当然我们也可以启动Spring容器的时候不传入配置文件,如果不传入的话getConfigLoactions()
会调用子类XmlWebApplicationContext
的getDefaultConfigLocations()
返回默认位置/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相关文章进行修改更新