从入职第一天起就得知咱公司的核心开发框架SOFA,而SOFA是基于Spring容器框架的。在使用SOFA一段时间以后,最近有兴趣研究一下Spring的源代码。本文主要将调试过程详细记录下来,一来加深印象,二来将经验沉淀下来,希望能对大家有所帮助。
本文结构如下:
1:新建Spring工程用于调试
2:IoC容器初始化代码调试
2.1 资源的定位
2.2 资源的解析
2.3 资源的注册
1.新建Spring工程用于调试
本人开发IDE为Intellij,所以本文基于Intellij建立一个Spring IOC实例用于调试IOC代码。
首先在Intellij上选择File->New->Project,如下图1所示:
图1:新建Spring工程
选择Spring->Spring MVC->Next如下图2所示:
图2:给Spring工程命名
项目生成好以后在src目录下新建package:com.alipay.spring.web 并新建几个类,如下图3所示
图3:新建Spring工程相关类-UserDAO类
图4:新建Spring工程相关类-UserDAOImpl类
图5:新建Spring工程相关类-UserManager类
图6:新建Spring工程相关类-UserTest类
在applicationContext.xml中声明相关bean,如图7所示:
图7:在applicationContext.xml中声明bean
值得注意的是:applicationContext.xml必须放在src目录下,否则UserTest类会找不到applicationContext.xml而报错。
下面我们运行一下这个项目,运行结果如图8所示:
图8:Spring工程运行结果
我们发现在控制台打印出了:”add a user”。那么这个是怎么打印出来的?这就是我们要研究的问题。
2.IOC容器初始化代码调试
我们可以把IOC容器看做一个有隔间的水桶,这个水桶可以装各种各样的水——盐水,糖水,纯净水……但是要让这个水桶发挥作用,必须满足一些条件:首先,这个水桶能找到水源;其次,这个水桶能把这些水分门别类装配到水桶不同的隔间里面;最后这个水桶能给打水的人提供装配好的水。(自己理解的,不当之处还请指正)。
根据上面的类比,我们把IOC容器的初始化分为三个部分
1) 资源的定位
2) 资源的解析
3) 资源的注册
在我们这个项目中,资源就是applicationContext.xml。Spring容器首先会对它进行定位,确定他的位置。然后他会对applicationContext.xml进行解析,解析成Spring能够识别的数据结构。然后Spring会把解析好的数据结构注册到Spring容器中以备下次调用。下面我们分三步进行调试。
2.1 资源的定位
我们在main函数的第一行打上断点,开启调试模式,如下图9所示:
图9:开启调试模式
接下来,我们进入ClassPathXmlApplicationContext这个类,它有一个构造函数,入参是一个String,如图10所示:
图10:一个入参的ClassPathXmlApplicationContext构造函数
它会调用另外一个3个入参的构造函数,如图11所示:
图11:三个入参的ClassPathXmlApplicationContext构造函数
在这个构造函数中,会有一个setConfigLocation函数,入参就是我们传入的”applicationContext.xml”。我们进去看看这个函数的实现,如图12所示:
图12:setConfigLocations函数实现
在这个函数中,会把传入的locations赋值给configLocations这个变量,这个变量就是用于存储资源位置的地方,以后想要获取资源,直接从这个读取这个变量就行了。这就相当于已经找到水源,并进行了标记,下面就要对这个水源进行装配。
我们可以画出资源定位的时序图,如图13所示:
图13:资源定位流程图
2.2 资源的解析
这一步在IOC容器的初始化过程中至关重要,也是最核心的一步。我们继续从刚才的ClassPathXmlApplicationContext构造函数着手,如图14所示:
图14:三个入参的ClassPathXmlApplicationContext构造函数
我们进入这个构造函数的refresh()函数,如下图15所示:
图15:refresh()函数实现
refresh()很长,但是与我们资源的解析和注册都在这个函数里面,我们进入这个函数,如图16所示:
图16: obtainFrshBeanFactory函数实现
在obtainFrshBeanFactory函数主要是构造好beanFactory,然后获取这个beanFactory返回给上层调用,我们先看getBeanFactory()函数实现,如图17所示:
图17:getBeanFactory()函数实现
由代码可知,getBeanFactory基本上就是获得beanFactory这个字段并返回,也就是说refreshBeanFactory就是为了设置好beanFactory这个字段,至于如何设置这个beanFactory,我们继续看refreshBeanFactory函数代码实现,如图18所示:
图18:refreshBeanFactory函数实现
这个函数的第一个if条件判断是用来清理已存在的BeanFactory,以便创建新的BeanFactory.接下来会创建一个新的空的beanFactory,如下截图19所示:
图19:创建一个初始化的BeanFactory
这一步相当于在水桶中已经准备好一个隔间,下面就要根据标记的水源把资源load到隔间中,而这个load的过程就在loadBeanDefinitions.我们进去看它的实现,如图20所示
图20:loadBeanDefinitions第一层实现
该函数首先创建一个XmlBeanDefinitionReader用于从标记的资源地获取资源。下面我们继续看看他是如何读取资源的,如图21所示
图21:loadBeanDefinitions第二层实现
这个函数主要用于获取资源标记,根据上图12,我们把包含bean的applicationContext位置存放在configLocations变量中,所以该函数的getConfigLocation就能获取资源地址,函数实现如图22所示:
图22:getConfigLocations函数实现
获得资源地址后如下图23所示:
图23:获取资源地址
然后我们看reader是如何对这个资源进行解析的,如下图24所示:
图24:初始化ResourceLoader
从图24可以看出,函数首先将String类型的location初始化一个ResourceLoader对象,并进一步得到Resouce对象。这个对象封装了对XML文件的I/O操作,所以读取器可以在打开I/O流后得到XML的文件对象。读取实现如图25所示:
图25:Resource的文件操作
正常情况下,文件操作会得到一个Document类型的XML文件对象,有了这个文件对象后,就可以按照Spring的Bean定义规则来对这个XML的文档树进行解析了,这个解析是交给BeanDefinitionParserDelegate来完成的。我们先来看看这个BeanDefinitionParserDelegate这个对象模型,如图26所示:
图26:BeanDefinitionParserDelegate对象模型
从图26可以看出,BeanDefinitionParserDelegate对象模型就是在applicationContext.xml中定义一个bean的各个属性。下面我们看看如何根据BeanDefinitionParserDelegate来解析applicationContext.xml,如图27所示:
图27:bean解析过程
这个函数里面就是详细解析xml文件各个节点的地方。首先,该函数会根据className生成一个初始化的BeanDefinition,然后给这个BeanDefinition一步步增加节点元素。比如parseMetaElements就是解析key-value的地方,parseBeanDefinitionAttributes就是详细解析bean的定义的地方。如图28所示:
图28:xml文件详细解析
经过图28的层层解析,就会得到一个具有详细节点信息的BeanDefinition,并设置到BeanDefinitionHolder中去,如图29所示:
图29:BeanDefinitionHolder对象
上面介绍了对bean元素进行解析的过程,也就是BeanDefinition依据XML的定义被创建的过程。这个BeanDefinition可以看成是对定义的抽象。这个数据对象中封装的数据大多都是定义相关的,也有很多就是我们在定义bean时看到的那些spring标记,如key-value,init-method,destory-method等等。这个BeanDefinition数据类型是非常重要的,它封装了很多基本数据,这些基本数据都是IoC容器需要的。有了这些基本数据,IoC容器才能对Bean配置进行处理,才能实现想应的容器特性。
经过上述载入过程,IoC容器大致完成了管理Bean对象的数据准备工作。也就是说基本上把水放进了隔间。但是,严格的说,这时候的容器还没有完全起作用,要完全发挥容器的作用,还需要完成数据向容器的注册。
我们可以根据上面的调试步骤画出资源解析的流程图,如图30所示:
图30:资源解析流程图
2.3 资源的注册
IoC容器的注册过程是通过一个HashMap来持有载入的BeanDefinition的。具体的注册函数如图31所示:
图31:容器注册入口
我们进入这个入口,看具体的函数实现,如图32所示:
图32:容器注册实现
这个容器注册过程其实并不复杂,就是把解析得到的BeanDefinition设置到hashMap中去。完成了BeanDefinition的注册,就完成了IoC容器的初始化过程。此时这些BeanDefinition已经可以被容器使用了,他们都在beanDefinitionMap里被检索和使用,容器的作用就是对这些信息进行处理和维护,这些信息是容器建立依赖反转的基础。
最后,我们可以根据上述步骤画出资源注册的流程图,如图33所示
图33:资源注册流程图