前言
本文是Spring源码解析IoC部分的第一篇文章,以最简单的IoC案例作为切入点,主要分析了XML配置文件是如何被加载的,Bean工厂是如何创建的
为了分析方便,本文创建了一个普通java类Student
,有String
类型name
和int
类型的age
两个属性,此外还有一个公共无参,无返回值的void study()
方法,初学时不管有多少种加载配置文件的方法new ClassPathXmlApplicationContext(String)
必然是最常用的一种。我们在resources
目录下创建了名为beans.xml
的Spring配置文件
在配置文件中使用
表明将
Student
交给Spring来管理,这里需要插一嘴,IoC最主要的作用是将繁琐的创建对象的过程与代码需要真正表达的业务逻辑解耦,所谓的创建对象过程还要分为两部分:1.构建出对象实体;2.建立对象间依赖关系,其中第二部分主要通过依赖注入
DI
实现,所以说很多书上会将
DI
作为
IoC
实现的手段。完成配置后我们就可以通过一段简单的代码得到托管的
Student
,并调用其中的
study()
既然第二句已经获取被托管的对象,那么
IoC
的主流程必然存在于第一句中,我们就以
new ClassPathXmlApplicationContext(String)
为入口,看看Spring是如何完成控制反转的
图中最后一个构造器才是逻辑真正的开始位置,第一个参数为配置文件数组,第二个参数表示是否需要刷新Spring上下文标识,这里为true,第三个参数为当前
context
的父上下文。在进一步深入之前我需要先给出
ClassPathXmlApplicationContext
的类图,类图就像指南针,当我们深入细节迷失时会引导我们识别正确的道路
图3中
setConfigLocations(configLocations)
主要作用是解析
ClassPathXmlApplicationContext(String)
参数中的配置文件路径,并将配置的单个或者多个配置文件存放在父类
AbstractRefreshableConfigApplicationContext
的成员变量
configLocations
数组内。如果路径中存在
${}
占位符,会用正确的值进行替换,由于其处理过程和使用
引入外部配置文件处理流程一致,所有将在后面解析
原理时单独讲解。设置完配置文件后调用
refresh()
刷新整个Spring上下文信息
该方法包裹在同步代码块中,防止整个Spring上下文刷新时多个线程造成的冲突,在查看对象监视器
startupShutdownMonitor
的使用时发现,在同类的
close()
中也使用了该对象监视器的同步代码块,这也引出了另一层含义,刷新Spring上下文相当于“创建”,而
close()
相当于”销毁”,共享同一个对象监视器保证了对Spring上下文的两种“侵入性”操作不能共存。此外该方法在
AbstractApplicationContext
中,很明显的模板方法设计模式,
prepareRefresh()
进行刷新前的准备工作
方法中
initPropertySources()
为空实现不用理会,
getEnvironment()
得到一个”标准环境对象”
StandardEnvironment
,而
validateRequiredProperties()
负责校验加载时配置文件或者系统参数中存在必须配置但没有配置的情况,所有必须配置的属性存储在
AbstractPropertyResolver
中的成员变量
Set requiredProperties
中,如果检测到需要配置而未配置的属性抛出
MissingRequiredPropertiesException
。接着看图5中
obtainFreshBeanFactory()
该方法顾名思义就是获得新的
BeanFactory
,总体实现思路超级简单:1.创建
BeanFactory
,由
refreshBeanFactory()
完成;2.取出新的
BeanFactory
返回,由
getBeanFactory()
完成。先看看第一步
首先根据
hasBeanFactory()
判断成员变量
beanFactory
是否为null,该方法由
beanFactoryMonitor
对象监视器的同步块包裹,如果已经存在bean工厂先进行销毁,之后调用
createBeanFactory()
创建新的工厂,该方法中
new
了
BeanFactory
接口的子类
DefaultListableBeanFactory
,该类是bean工厂的对外暴露的核心类,这里同样给出该类的类图,供读者在源码中迷失时指引方向
我们从图中发现
DefaultListableBeanFactory
不仅实现了
BeanFactory
接口,同时也实现了
BeanDefinitionRegistry
,在开篇中我们曾经说过,对象存放在
BeanFactory
中,而“放”这个动作由
BeanRegistry
完成,现在两者统一合成到了一个类中。这就像刚学Java面向对象思想的时候老师举过的一个例子,人去关灯这件事,如果要用面向对象的思想表示,”关灯”这个动作应该是封装在灯对象中而不是人对象,随着源码分析的不断深入,我们还会进一步剖析
DefaultListableBeanFactory
创建完后给bean工厂设置唯一标识,该标识和实现
Serializable
后生成的
serialVersionUID
作用相似,都是保证序列化和反序列化后的对象一致性。
该方法用于设置
BeanFactory
的自定义属性,其实里面就两个:
allowBeanDefinitionOverriding
,当多个配置文件中存在同一个
id
或者
name
标识的标签时是否用后加载配置文件中的内容覆盖先加载的,我们可以在创建
ClassPathXmlApplicationContext
后自主设置,注释上说设为
false
就会发生覆盖,设为
true
当出现重复时会抛出异常,但恕我愚笨,无论怎么模拟都只发生了覆盖但不抛出异常,希望读者有知道的给我说说如何模拟;
allowCircularReferences
表示是否允许循环依赖,说到这个循环依赖我可是吃过它的亏。在第一家公司工作的时候项目组织的很混乱,
Service
层有时依赖
Dao
,有时又图省事
Service
之间相互依赖,时不时出现循环依赖问题,造成项目无法启动,关于这种问题如何解决我分享给大家一篇文章 Circular Dependencies in Spring,里面提供了多种解决思路,这里不再赘述。最后一句代码也是非常重要的,看着名字大家十有八九都能猜到它的功能与
@Autowired
和
@Qualifier
相关。在Spring中特别喜欢用
xxxResolver
、
xxxHandler
给类命名,表示对某个东西的处理类,注解的实现和运行原理也会专门开一篇详述,这里不往下深入。回到图8
loadBeanDefinitions(DefaultListableBeanFactory)
封装了读取配置文件并解析的流程
该类的具体实现在父类
AbstractXmlApplicationContext
中(如果迷失请看类图),第一句创建了
XmlBeanDefinitionReader
的实例,和开篇我们推测的一样,主要功能是将配置文件读取到内存中,之后为其设置“当前运行环境”,其实就是上面设置过的
StandardEnvironment
。
ResourceEntityResolver
代表配置文件的解析器,xml的语法定义约束有两种:DTD和XSD,在该解析器的父类
DelegatingEntityResolver
中就分别定义了两种特定类型的解析器
此外在初始化
PluggableSchemaResolver
中会加载默认
schemas
,位置在每个Spring模块的
META-INF/spring.schemas
,比如对于
3.2.8.RELEASE
的
spring-beans
模块,默认的
schemas
如下
initBeanDefinitionReader(XmlBeanDefinitionReader)
内设置默认解析xml配置文件需要验证,流程走到
loadBeanDefinitions(XmlBeanDefinitionReader)
按照本文的例子启动Spring容器时
configResources
为空,所有的配置文件都存放在
configLoactions
数组中,当然我们也可以启动Spring容器的时候不传入配置文件,如果不传入的话
getConfigLoactions()
会调用子类
XmlWebApplicationContext
的
getDefaultConfigLocations()
返回默认位置
/WEB-INF/applicationContext.xml
的配置文件。随后用参数的reader加载配置文件
流程走到
XmlBeanDefinitionReader
的父类
AbstractBeanDefinitionReader
的上图中方法,第一个参数是每一个配置文件,第二个参数为null。这里的
resourceLoader
就是
ClassPathXmlApplicationContext
的实例,因为该实例实现了
ResourceLoader
接口,具体的赋值过程在图11中,流程进入红框处代码,省略中间很多非重点调用,最终创建
BeanDefinitionDocumentReader
实例解析xml文件并进行
BeanDefinition
的注册
标注1是进来的第一步,
Document
是Spring使用
SAX
解析xml之后得到的
Document
对象,然后获取xml的根元素对象并传入标注2方法,首先处理
Document
中的
profile
属性,使用该属性的人可能不多,他就像
maven
中的
组合,可以根据运行的不同环境加载不同
profile
的值。之后创建一个委派类
BeanDefinitionParserDelegate
进行xml文件解析的初始化,其中主要对顶层
内的属性进行解析,在
BeanDefinitionParserDelegate
中存储了很多常量,每一个常量都与xml中一个标签或者属性对应,其中就有一组
中的属性
属性具体的用法请读者翻阅Spring in Action或者其他资料,这里只分析流程不做详细解释,回到图16。
preProcessor(Element)
和
postProcessXml(Element)
分别用户子类继承重写对xml解析进行前置处理、后置处理。
parseBeanDefinitions(Element, BeanDefinitionParserDelegate)
对我们常用的,诸如
、
进行解析
后记
为了突出主要流程,在文中留了很多可继续深挖的扩展点,这些扩展点后期会用“外传”的形式另起文章分析。随着源码阅读的深入,对之前文章中知识点的理解必定也会有所不同,所以之后会随时对Spring相关文章进行修改更新