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相关文章进行修改更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350