debug方式深入spring ioc容器的源码实现

      前言

      转载请标明出处 http://www.jianshu.com/p/33f4e4fb5c93 。

      第一次写文章,没啥经验,排版和代码黏贴都无力吐槽,网上关于spring源码解读的好文章一大堆,当然自己本身水平也是很一般的,我写的这个,最主要的还是为了做个笔记,防止自己忘记,偶尔还可以复习一下。

        spring太庞大了,源码很大,有各种各样的实现细节,太琐碎,所以阅读spring的源码要事无巨细,理解原理即可,否则就可能在读源码的过程中掉到各种细节里出不来。

        在研究spring的源码前,建议可以先把spring framework reference看一看,毕竟这是官方的开发参考手册,很多东西写的也是很透彻。也可以看看讲spring源码的几本书比如《spring源码深度解析》、《spring技术内幕_深入解析spring架构与设计原理》,debug 的时候配合着看书,对理解和记忆都是非常高效的。

        为什么要用debug呢,debug是最直接的跟着程序运行一步一步走下去的,可以清楚地知道代码这个时候在哪里,在执行什么逻辑,变量值是什么,类型是什么,当然,最主要的是以前在学习springsecurity的时候用debug走了好几遍尝到了甜头,给了我很好的体验。

      容器的基本实现

        以spring ioc作为开篇,控制反转在spring中是一个非常核心的功能,以下摘抄自spring framework reference:在Spring中,BeanFactory是IoC容器的核心接口。 它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。Spring为我们提供了许多易用的BeanFactory实现,XmlBeanFactory就是最常用的一个。该实现将以XML方式描述组成应用的对象 以及对象间的依赖关系。XmlBeanFactory类将获取此XML配 置元数据,并用它来构建一个完全可配置的系统或应用。

         在这就拿XmlBeanFactory进行debug,从网上找了个例子(这里我采用的是spring4.2.3,因为以前搭好了一个spring4.2.3的maven项目,懒得换了):


Foo.java

public class Foo {

public void execute(){

System.out.println("foo execute");

}

}


TestFoo.java

public class TestFoo {

@Test

public void testExecute(){

BeanFactory factory = new XmlBeanFactory(new ClassPathResource("testbean.xml"));

Foo bean = (Foo) factory.getBean("foo");

bean.execute();

}

}


testbean.xml  

(不知为啥粘贴不了,手打最主要的吧)

<bean id="foo",class="com.spring.test.Foo"/>


开始打断点debug运行,


      可以看到,debug时第15行点击step into 或者F5将会跳入ClassPathResource的构造函数,为什么使用ClassPathResource呢,因为spring的配置文件读取是通过ClassPathResource进行封装的,spring抽象了一个接口Resource用来封装底层资源,这里就不详细介绍了,有兴趣的可以自行查看源码。对于不同来源的资源文件都用相应的Resource实现,ClassPathResource就是其对应的classpath资源的实现。


这里调用内部的构造函数,

继续step over(f6),直到回到了开始的15行,点击step into,进入XmlBeanFactory的构造函数,与ClassPathResource一样调用了一个内部的构造函数,

这里可以看到,调用了一个父类构造函数初始化的过程super(parentBeanFactory),感兴趣的可以自己去跟踪代码,这里最主要的就是this.reader.loadBeanDefinitions(resource),资源加载的真正实现,继续step over,可以看到这个reader

通过使用XmlBeanDefinitionReader从XML配置文件中读取bean的定义,直接进入loadBeanDefinitions(resource)方法,

继续step into,进入EncodedResource(resource),就是对resource使用encodedResource类进行封装,主要作用是对资源文件的编码进行处理,很简单,没什么可看的,接着下一步计入loadBeanDefinitions方法中,由于.class文件无法编辑,不能写注释,所以我把这段代码复制出来写上了注释,大家可以对比一下。

可以看到在 return doLoadBeanDefinitions(inputSource, encodedResource.getResource());这里真正的进入了核心的逻辑部分,前面都是些获取inputsource的工作,step into  doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法中,

可以看到,这段核心代码就只有上图中框住的两行,这里要注意的是由于spring的版本不同,这一块的代码可能是不同的,但是实际上的原理是一致的,理解就可以了,进入doLoadDocument方法中,

可以看到这个loadDocument方法带有5个参数,先简单介绍一下这几个参数的作用,


getEntityResolver()这个主要是根据不同的验证模式获取不同的的解析器解析,其主要是用来根据配置文件头上的那些声明,类似于下图,去找到工程中对应的那些dtd文件和xsd验证文件

this.errorhandler:看名字就是一个错误处理器

getValidationModeForResource(resource):获取XML的验证模式,返回值int类型。

isNamespaceAware():Return whether or not the XML parser should be XML namespace aware(返回一个boolean的值表示xml解析器是否知道xml的namespace)


点击step into,进入了

这里就不详细介绍了,这一块有兴趣的可以参考《spring源码深度解析》的2.7.1章节。

继续step into,进入了getValidationModeForResource(resource),

可以看到validationModeToUser=1,而这里几个常量值分别如下:VALIDATION_AUTO=1,VALIDATION_XSD=3,第一个if表示如果手动指定了验证模式则使用指定的验证模式,第二个if表示如果未指定则使用自动检测,这里debug继续走可以看到,detectedMode=3,

进入了第二个if,表明使用的是自动检测模式。

继续下一步下一步。。。,进入加载Document步骤了,这里的Document对象其实就是html的dom的Document对象。

这里没什么好讲的,感兴趣的可以自己去找资料或者自己看源码的实现细节。首先创建DocumentBuilderFactory对象,然后通过DocumentBuilderFactory对象创建DocumentBuilder对象,最后通过DocumentBuilder解析inputsource返回Document对象。

兜兜转转回到了doLoadBeanDefinitions(inputSource, encodedResource.getResource())的位置,我们已经完成了解析Document对象的步骤,现在开始提取并注册bean,step into  registerBeanDefinitions 方法中,

countBefore记录统计前加载的beandefinitions的个数

这里先实例化一个BeanDefinitionDocumentReader,这是一个接口,而通过createBeanDefinitionDocumentReader(),BeanDefinitionDocumentReader的类型已经变成了DefaultBeanDefinitionDocumentReader了,这个方法的主要目的提取Element类型的root元素,以便将root元素作为参数继续进行bean的注册。

继续进入doRegisterBeanDefinitions(root),这里才算的上开始真正的进行解析了。

这里我们的bean没有profile属性,直接跳过处理profile属性,进入parseBeanDefinitions(root,this.delegate),

在spring的XML配置文件中有两大类Bean的声明,一个是默认的如:

<bean id="foo",class="******.Foo"/>

另一类就是我们自定义的,

<tx:annotation-driven/>

而这两种方式的读取与解析差别是非常大的,对于根节点或者子节点采用的是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用parseCustomElement对自定义命名空间进行解析。自定义命名空间的判断方法是通过node.getNamespaceURI()获取命名空间,并与spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一直则认为是默认,否则自定义。

下面是默认解析方式:

这里我们的testbean.xml只有bean标签,说以进入的是第三个判断对bean标签的解析,对bean标签的解析也是这四个中最复杂的一个,理解了这个其它的也很简单了,现在step into processBeanDefinition方法中,

step into 第一行的parseBeanDefinitionElement方法中,这一步是通过委托类的parseBeanDefinitionElement方法进行元素解析,创建bdHolder,

直接来到460行,前面是完成了对id属性和name属性的解析,很简单,第460行是对bean标签进一步的解析,包含对lookup-method,构造函数等等bean标签所包含的所有属性都进行了解析(感兴趣的可以自行查看源码或者观看《spring源码深度解析》3.1.1章节),是把beandefinitions封装到了GenericBeandefinition中,

我们直接debug看解析完成后的bd的值,

可以看到beandefinition的类型是GenericBeandefinition,并且它的值包含了bean标签的所有属性值,GenericBeandefinition是spring2.5后新加入的用于承载bean属性的beandefinition。

后面的代码则是一些对beanname的逻辑处理以及最后将解析出来的各种信息封装到BeanDefinitionHolder中。

继续下一步。。。,回到processBeanDefinition方法中,这是进行到了第297行,

这一步是指 若默认标签的子节点下有自定义属性,则还需要对自定义的标签进行解析。

接下来是将解析后的beandefinition注册了,step into registerBeanDefinition方法

step into registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()),根据beanname注册beandefinition,这里将我个人理解的注释后的源码贴出,有不对的或者不妥帖的地方请大家指出:

好了,根据beanname注册beandefinition解析完了,点击下一步,到了对alias别名进行解析(此处我的代码不存在别名,但还是提一下),如果理解了上一步的根据beanName解析的话,这里其实很简单,

继续点击下一步。。。,回到processBeanDefinition最后一行逻辑代码getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)),这里是同志监听器解析注册完成。

到这里为止,其实关于默认标签的解析就完成了,后面还有一些无关紧要的代码,我们直接跳过,而至于自定义标签的解析以及后面的一个非常重要额度bean的加载将在后面的章节中进行解析。

小结

      由于我们使用的是XmlBeanFactory,而XmlBeanFactory是继承自DefaultListableBeanFactory,这也spring默认使用的beanfactory,而DefaultListableBeanFactory已经完成了注册beandefinition的功能,XmlBeanFactory则实现了一个XmlBeanDefinitionReader的reader用于读取Xml资源的resource,将其解析成Document,并最终将Document的元素Element解析成beandefinition,上面的整个过程可以简单的概括为:


ClassPathResource读取xml成resource---->XmlBeanDefinitionReader解析resource到Document---->解析Document到BeanDefinition---->注册beandefinition


推荐阅读更多精彩内容