IoC容器的初始化流程分析

前言:本文参考了《Spring核心内幕 - 深入解析Spring架构与设计原理(第2版)》一书,结合笔者对源码的阅读得到的关于Spring IoC容器初始化流程的理解。

一、什么是控制反转(IoC,Inversion of Control)?

    在进入正题之前,首先我们有必要先了解一下控制反转这个概念,“反转”的是什么?为什么要进行控制反转?

1.传统的对象依赖关系管理

public class MyService {
    public void service() {
        // do something...
    }
}
    这是一段简单的Java代码,设想这是一个封装了业务逻辑的Javabean,实际的应用中可能包含许多类似的Javabean。在Spring之前,如何去调用其中的业务逻辑呢?很自然地我们会想到在调用方中通过new关键字来初始化Javabean对象,如:
MyService myService = new MyService();
myService.service();

    仔细分析以上代码,可得到传统的对象依赖关系管理的弊端?

1.)代码耦合度高;每处需要使用这个Javabean的地方都会与其产生耦合。

2.)可测试性差;例如要测试一个主业务流程,需要调用多个Javabean封装的子流程,那么一个头疼的问题是你需要初始化所有的Javabean,并进行组装,随后才能对主流程进行测试。

3.)重复代码多;对于同一个Javabean,所有用到的地方都需要通过new关键字和其setter方法进行初始化,其中便会产生许多重复代码。

4.)冗余对象多;事实上,这些封装了业务流程的Javabean,基本都是无状态的。也就是说,对于一个Javabean,我们的应用可能仅需要一个单例对象。

2.基于Spring IoC容器的对象依赖关系管理

    为了应对传统方式的弊端,便有了IoC容器,其核心理念即是控制反转。将对依赖的控制从具体业务对象手中转交到平台或框架中,需要的时候再由平台或框架注入到具体业务对象中。

<bean id="myService" class="test.MyService"/>

    Spring还支持通过XML对Javabean进行配置,在XML中配置Javabean后,便可以通过Spring注解将其注入到所有需要的地方了。在依赖控制权反转到IoC容器后,上面提到的几个问题也就解决了。

1.)代码耦合度高;使用XML配置注解结合的方式,具体的业务对象已不会与具体的Javabean耦合。

2.)可测试性差;使用XML配置注解结合的方式,IoC容器会自行为我们组装具体的业务对象,不需要手动干预。

3.)重复代码多;对于Javabean初始化的这段重复代码,被IoC容器抽象出来并作为Spring的基础设施了。

4.)冗余对象多;IoC容器可以支持单例对象。


二、Spring的IoC容器

    通过上面的介绍,我们已经对IoC(控制反转)有了一定的了解。在继续往下之前,我们先对IoC容器下一个简单的定义:IoC容器是Spring的核心模块,是抽象了对象管理、依赖关系管理的框架解决方案。

    在Spring IoC容器的设计中,有两个主要的容器系列,一个是实现了BeanFactory接口的简单容器系列,这系列容器只实现了容器的最基本功能;另一个是ApplicationContext应用上下文,它作为容器的高级形态而存在,除了BeanFactory的基本功能,还提供了更多丰富的功能,常用的有ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、XmlWebApplicationContext。

    Spring IoC容器的核心接口设计:


    由接口设计,我们看到ApplicationContext除了继承BeanFactory的子接口,还继承了ResourceLoader、MessageSource等接口,因此提供的功能也就更丰富了。


三、IoC容器的初始化流程

    IoC容器是Spring的核心模块,集成了对象管理和依赖关系管理。因此,我们可以大胆推测,IoC容器的启动必是Spring容器启动的第一环节。下面我们以ClasspathXmlApplicationContext为例,深入源码说明IoC容器的启动流程。

1.首先看一下ClasspathXmlApplicationContext类的继承体系(图中只注明了部分关键方法、属性及接口)。核心类是AbstractApplicationContext。此图仅作后续时序图的参照,无需过分纠结。

2.在流程分析前,我们先给出IoC容器初始化的主体流程脉络,有兴趣的同学后面可以跟着此脉络和时序图回归源码自行分析。

1.)Resource定位;指对BeanDefinition的资源定位过程。通俗地讲,就是找到定义Javabean信息的XML文件,并将其封装成Resource对象。

2.)BeanDefinition的载入;把用户定义好的Javabean表示为IoC容器内部的数据结构,这个容器内部的数据结构就是BeanDefinition。

3.)向IoC容器注册这些BeanDefinition。

3.从ClasspathXmlApplicationContext构造函数开始看,时序图如下:

1.)AbstractApplicationContext#refresh:此方法定义Spring启动的“提纲”,包含Spring容器启动的一系列工作。这里我们直接看obtainFreshBeanFactory方法,这是IoC容器初始化的入口。

2.)AbstractApplicationContext#obtainFreshBeanFactory:调用refreshBeanFactory和getBeanFactory方法,重新获取一个新的BeanFactory实例。

3.)AbstractRefreshableApplicationContext#refreshBeanFactory:销毁原BeanFactory,新建DefaultListableBeanFactory实例,并对其进行一些初始化工作。

4.)AbstractXmlApplicationContext#loadBeanDefinitions:上一步创建BeanFactory实例之后,通过该BeanFactory创建XmlBeanDefinitionReader实例,也就是ApplicationContext上下文将BeanDefinition的定位加载工作交付到了XmlBeanDefinitionReader。继续跟到重载的同名方法loadBeanDefinitions(XmlBeanDefinitionReader reader),这一步完成了将部分XML文件资源封装为Resource对象,而用户以XML文件路径传入的BeanDefinition资源文件也会在AbstractBeanDefinitionReader#loadBeanDefinitions(String...locations)逐一封装为Resource对象,并进行进一步的处理。

以上 1.) ~ 4.)步完成了Resource定位的工作。

5.)继续跟踪到XmlBeanDefinitionReader#loadBeanDefinitions:这一步主要是通过Resource对象获取XML文件的输入流,继续执行doLoadBeanDefinitions。因为是XML文件格式组织的BeanDefinition,因此AbstractBeanDefinitionReader需要委托到其子类XmlBeanDefinitionReader。

6.)XmlBeanDefinitionReader#doLoadBeanDefinitions:通过上一步获取到的输入流和Resource对象,获取Document对象。

7.)XmlBeanDefinitionReader#registerBeanDefinitions:初始化DefaultBeanDefinitionDocumentReader对象,将上一步得到的Document对象交由DefaultBeanDefinitionDocumentReader对象解析,并统计解析的BeanDefinition个数。

8.)DefaultBeanDefinitionDocumentReader#registerBeanDefinitions:获取Document对象的根元素,执行doRegisterBeanDefinitions。

9.)继续跟到DefaultBeanDefinitionDocumentReader#parseBeanDefinitions:初始化BeanDefinitionParserDelegate实例,递归解析Document。DefaultBeanDefinitionDocumentReader负责读入Document对象,实际将Document解析为BeanDefinition的却是BeanDefinitionParserDelegate对象。

10.)继续跟到DefaultBeanDefinitionDocumentReader#processBeanDefinition:传入Document对象的单个Element元素,将其交由上一步的BeanDefinitionParserDelegate对象解析,解析后得到BeanDefinitionHolder对象。解析过程较为复杂,有兴趣可以自行跟踪。

以上 5.) ~10.)步完成了BeanDefinition的载入工作,得到BeanDefinitionHolder对象。

11.)顺着DefaultBeanDefinitionDocumentReader#processBeanDefinition继续往下看,跟到BeanDefinitionReaderUtils#

registerBeanDefinition。传入上一步的BeanDefinitionHolder对象,将BeanDefinition注册到IoC容器中。

以上11.)完成的是向IoC容器注册这些BeanDefinition,BeanFactory是以Map的结构组织这些BeanDefinition的。可以在DefaultListableBeanFactory中看到此Map的定义。

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);

以上就是Spring IoC容器的整个启动过程,这里完成的仅仅是BeanDefinition的载入和注册,Javabean之间的依赖关系并不会在初始化的时候完成!


四、总结

1.)AbstractApplicationContext的refresh方法定义了Spring容器启动的“提纲”,脉络清晰,值得学习借鉴。

2.)《Spring技术内幕》一书,从整体到局部的组织说明顺序值得学习借鉴。

3.)Spring IoC的启动过程可以看到,类的命名清晰,职责划分明确,值得学习借鉴。例如代表XML文件资源的Resource对象由XmlBeanDefinitionReader负责读取。Document对象由DefaultBeanDefinitionDocumentReader读取,由BeanDefinitionParserDelegate解析。这种清晰的职责划分让我们很容易选择关注的点,而抛弃不需要过多注意的细节。

4.)IoC容器的依赖反转,类似于工厂方法模式。

5.)以HashMap维持Javabean的单例,减少了冗余对象。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_34596644/article/details/80394209
个人分类: Spring
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭