Spring的容器就是bean容器也是IOC容器,它的初始化过程非常复杂,设计到复杂的类继承关系,在此简单分析一下关键的类和步骤。
一、核心数据结构
1、Resource
在Spring内部,针对于资源文件有一个统一的接口Resource表示。其主要实现类有ClassPathResource、FileSystemResource、UrlResource、ByteArrayResource、ServletContextResource和InputStreamResource。Resource接口中主要定义有以下方法:
- exists():用于判断对应的资源是否真的存在。
- isReadable():用于判断对应资源的内容是否可读。需要注意的是当其结果为true的时候,其内容未必真的可读,但如果返回false,则其内容必定不可读。
- isOpen():用于判断当前资源是否代表一个已打开的输入流,如果结果为true,则表示当前资源的输入流不可多次读取,而且在读取以后需要对它进行关闭,以防止内存泄露。该方法主要针对于InputStreamResource,实现类中只有它的返回结果为true,其他都为false。
- getURL():返回当前资源对应的URL。如果当前资源不能解析为一个URL则会抛出异常。如ByteArrayResource就不能解析为一个URL。
- getFile():返回当前资源对应的File。如果当前资源不能以绝对路径解析为一个File则会抛出异常。如ByteArrayResource就不能解析为一个File。
- getInputStream():获取当前资源代表的输入流。除了InputStreamResource以外,其它Resource实现类每次调用getInputStream()方法都将返回一个全新的InputStream。
实现类:
- ClassPathResource可用来获取类路径下的资源文件。假设我们有一个资源文件test.txt在类路径下,我们就可以通过给定对应资源文件在类路径下的路径path来获取它,new ClassPathResource(“test.txt”)。
- FileSystemResource可用来获取文件系统里面的资源。我们可以通过对应资源文件的文件路径来构建一个FileSystemResource。FileSystemResource还可以往对应的资源文件里面写内容,当然前提是当前资源文件是可写的,这可以通过其isWritable()方法来判断。FileSystemResource对外开放了对应资源文件的输出流,可以通过getOutputStream()方法获取到。
- UrlResource可用来代表URL对应的资源,它对URL做了一个简单的封装。通过给定一个URL地址,我们就能构建一个UrlResource。
- ByteArrayResource是针对于字节数组封装的资源,它的构建需要一个字节数组。
- ServletContextResource是针对于ServletContext封装的资源,用于访问ServletContext环境下的资源。ServletContextResource持有一个ServletContext的引用,其底层是通过ServletContext的getResource()方法和getResourceAsStream()方法来获取资源的。
- InputStreamResource是针对于输入流封装的资源,它的构建需要一个输入流。
2、ResourceLoader
通过上面介绍的Resource接口的实现类,我们就可以使用它们各自的构造函数创建符合需求的Resource实例。但是在Spring中提供了ResourceLoader接口,用于实现不同的Resource加载策略,即将不同Resource实例的创建交给ResourceLoader来加载,这也是ApplicationContext等高级容器中使用的策略。
接口中有两个主要的方法:
- getResource():在ResourceLoader接口中,主要定义了一个方法:getResource(),它通过提供的资源location参数获取Resource实例,该实例可以是ClasPathResource、FileSystemResource、UrlResource等,但是该方法返回的Resource实例并不保证该Resource一定是存在的,需要调用exists方法判断。
- getResourceByPath:这个方法被声明为protected,所以在它的子类中基本都重写了这个方法。
2.1、实现类
DefaultResourceLoader是ResourceLoader的默认实现。
1 | public Resource getResource(String location) { |
其最主要的逻辑实现在getResource方法中:
- 该方法首先判断传入的location是否以”classpath:”开头,如果是,则创建ClassPathResource(移除”classpath:”前缀)
- 否则尝试创建UrlResource
- 如果当前location没有定义URL的协议(即以”file:”、”zip:”等开头,比如使用相对路径”resources/META-INF/MENIFEST.MF),则创建UrlResource会抛出MalformedURLException,此时调用getResourceByPath()方法获取Resource实例
- getResourceByPath()方法默认返回ClassPathContextResource实例,在FileSystemResourceLoader中有不同实现。
它有一个重要的子类就是AbstractApplicationContext,因为后面要介绍的ApplicationContext的几个实现类都继承自它,也就是说,比如FileSystemXMLApplicationContext这个类因为继承自DefaultResourceLoader,所以具备了它的getResource方法,在最后定位资源的时候都调用了这个方法来获取Resource。
3、BeanDefinition
一个BeanDefinition描述了一个bean的实例,包括属性值,构造方法参数值和继承自它的类的更多信息。
作用:持有bean数据结构,是注入的bean在IoC容器中的抽象,方法列表如下
1 | getBeanClassName; |
相关的类与接口:
- AbstractBeanDefinition:是BeanDefinition的一个完整实现
- BeanDefinitionReader:用于读取bean的信息的接口。AbstractBeanDefinitionReader实现了该接口,XmlBeanDefinitionReader继承了AbstractBeanDefinitionReader。
4、ApplicationContext
高级IoC容器,除了基本的IoC容器功能外,支持不同信息源、访问资源、支持事件发布等功能。
继承了以下接口:
- ListableBeanFactory:继承自BeanFactory,在此基础上,添加了containsBeanDefinition、getBeanDefinitionCount、getBeanDefinitionNames等方法。
- HierarchicalBeanFactory:继承自BeanFactory,在此基础之上,添加了getParentBeanFactory、containsLocalBean这两个方法。
- AutoWireCapableBeanFactory:继承自BeanFactory
- MessageSource:用于获取国际化信息
- ApplicationEventPublisher:因为ApplicationContext实现了该接口,因此spring的ApplicationContext实例具有发布事件的功能。
二、容器的初始化过程
IOC容器的初始化过程就是含有BeanDefinition的信息的Resource的定位、载入、解析、注册的四个过程,最终配置的bean以BeanDefinition的数据与结构存在于IOC容器之中,这个过程不涉及bean的依赖注入,也不产生任何bean。
以FileSystemXmlApplicationContext为例分析一下它的初始化过程。
先看一下它的类继承关系
1、Resource的定位过程
1.1、FileSystemXmlApplicationContext 类
一般当构造的时候调用使用new来创建实例,配置文件的的地址作为参数:
1 | ApplicationContext ctx1=new FileSystemXmlApplicationContext("c:/bean.xml"); |
先来看一下最下面的实现类FileSystemXmlApplicationContext源码
1 | package org.springframework.context.support; |
有三部分
- 继承自AbstractXmlApplicationContext,而它最终的父类是DefaultResourceLoader,所以它具备了Resource定义的BeanDefinition的能力
- 调用refresh方法,这个方法在父类AbstractApplicationContext中已经封装好了。它详细描述了整个ApplicationContext的初始化过程,比如BeanFactory的更新、MessageSource和PostProcessor的注册等。这里看起来像是对ApplicationContext进行初始化的模版或执行提纲,这个执行过程为Bean的生命周期管理提供了条件。
- 重写了getResourceByPath方法,该方法是一个模版方法,通过构造一个FileSystemResource对象来得到一个在文件系统中定位的BeanDefinition。
它的调用关系
refresh为初始化IoC容器的入口,但是具体的资源定位还是在XmlBeanDefinitionReader读入BeanDefinition时完成,loadBeanDefinitions() 加载BeanDefinition的载入。
1.2、AbstractRefreshableApplicationContext
1 | protected final void refreshBeanFactory() throws BeansException { |
通过loadBeanDefinitions(beanFactory);加载BeanDefinition信息,BeanDefinition就是在这里定义的。
AbstractRefreshableApplicationContext对loadBeanDefinitions仅仅只是定义了一个抽象的方法,真正的实现是在子类AbstractXmlApplicationContext中的。
1.3、AbstractXmlApplicationContext
1 | protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { |
可以看出:
- 首先创建一个BeanDefinitionReader
- 调用本类中的loadBeanDefinitions(XmlBeanDefinitionReader reader)
- 在loadBeanDefinitions(XmlBeanDefinitionReader reader)中,先获取资源的定位,然后将资源的位置作为参数,调用reader的reader.loadBeanDefinitions(configLocations)。这个方法在AbstractBeanDefinitionReader类里面实现
注意这个方法中的两个loadBeanDefinitions的参数是不一样的,一个是Resource数组,而另一个是String数组,数组里是bean资源定位的路径
1.4、AbstractBeanDefinitionReader
1 | public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { |
终于到了真正的定位方法了,可以看出,
- 首先获取了ResourceLoader,
- 然后调用resourceLoader.getResource(location),这样就完成了从String到Recourse的过程。
- resourceLoader.getResource(location)在最上面介绍的DefaultResourceLoader类中实现。
1.5、总结
至此,Resource的定位过程就完全完成了。
从最开始的FileSystemXmlApplicationContext类中重写的getResourceByPath方法的调用,可以看出它完整的过程。
- 从构造函数开始调用各个父类的方法
- 而各个父类方法在getResourceByPath方法时使用了模版方法,需要子类自己实现
- 所以,随后又调用到自己重写的这个getResourceByPath方法,完成Resource的定位。
可以感觉到,虽然各个类的继承关系特别复杂,一时间难以理清,但是理解之后发现这种面向对象的编程思想:父类定义整个流程,而具体的实现细节通过模版模式由子类具体实现,这样子类在实现的时候有着很大的灵活性,扩展起来极其方便。
这个图是最标准的方法调用过程。
2、BeanDefinition的载入
关于载入过程,要再次回到AbstractXmlApplicationContext的loadBeanDefinitions方法中
1 | protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { |
本来上面分析的是Resource[] configResources = getConfigResources();为空,所以不会执行但是不知道为什么都说是从这里开始执行加载过程的,虽然从逻辑上来看,确实是获取到Resource后开始载入过程。
2.1、XmlBeanDefinitionReader
首先看一下XmlBeanDefinitionReader中loadBeanDefinitions(Resource …)方法,
1 | //XmlBeanDefinitionReader加载资源的入口方法 |
可以看出,载入过程相对没有那么复杂,
- 先获取Resource的流
- 将流中的XML文件解析为Document对象,这个过程由documentLoader完成。
DocumentLoader将Bean定义资源转换成Document对象的源码如下:
1 | //使用标准的JAXP将载入的Bean定义资源转换成document对象 |
该解析过程调用JavaEE标准的JAXP标准进行处理。至此Spring IoC容器根据定位的Bean定义资源文件,将其加载读入并转换成为Document对象过程完成。
注意这个只是XML文件的解析,而不是BeanDefinition的解析。对BeanDefinition的解析是下一步,在registerBeanDefinitions(doc, resource); 中完成
2.2、总结
可以看出,载入过程实际上就是Resource对象转换成Document对象的过程,也就是一个XML文件解析的过程,Spring中使用的是JAXP解析,生成的Document文件就是org.w3c.dom.Document中的Document文件。
3、BeanDefinition的解析
在上面的XmlBeanDefinitionReader类中完成载入工作后,得到了Document对象,接下来调用registerBeanDefinitions(doc, resource)启动Spring IoC容器对Bean定义的解析过程。
1 | //按照Spring的Bean语义要求将Bean定义资源解析并转换为容器内部数据结构 |
该方法先获得一个BeanDefinitionDocumentReader,然后调用documentReader.registerBeanDefinitions(doc, createReaderContext(resource))方法,这个方法是在DefaultBeanDefinitionDocumentReader类中实现的。
3.1、DefaultBeanDefinitionDocumentReader
BeanDefinitionDocumentReader接口通过registerBeanDefinitions方法调用其实现类DefaultBeanDefinitionDocumentReader对Document对象进行解析,
1 | //根据Spring DTD对Bean的定义规则解析Bean定义Document对象 |
可以看出,Spring采用DOM方式解析xml文件,
- 从根节点开始逐个获得子节点Element,然后判断是不是根据Spring命名空间的
- 如果是,那么调用parseDefaultElement(ele, delegate); 使用Spring的Bean规则解析元素节点
- 如果不是,那么调用delegate.parseCustomElement(ele); 则使用用户自定义的解析规则解析元素节点
- 在Spring的Bean规则解析Document元素节点方法parseDefaultElement中:
- 如果元素节点是
<Import>
导入元素,调用importBeanDefinitionResource(ele)方法进行导入解析 - 如果元素节点是
<Alias>
别名元素,调用processAliasRegistration(ele)方法进行别名解析 - 对于既不是
<Import>
元素,又不是<Alias>
元素的元素,即Spring配置文件中普通的<Bean>
元素的解析,那么调用processBeanDefinition(ele, delegate)方法
- 如果元素节点是
- 在processBeanDefinition方法中,定义了一个BeanDefinitionHolder对象,用来持有对bean属性的处理结果,这个处理过程则由BeanDefinitionParserDelegate类中的parseBeanDefinitionElement方法来实现。
- 解析完成之后,就会将这个BeanDefinitionHolder对象作为参数调用BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())方法,来启动注册的过程。
关于import和ailas书中没有具体分析,主要是介绍了BeanDefinitionParserDelegate中的parseBeanDefinitionElement方法来解析Bean定义资源文件中的<Bean>
元素的过程
3.2、BeanDefinitionParserDelegate
1、id、name、alias属性的解析
processBeanDefinitionfang方法源码:
1 | //解析Bean定义资源文件中的<Bean>元素,这个方法中主要处理<Bean>元素的id,name |
到了这里,在IOC容器中存在的数据结构AbstractBeanDefinition beanDefinition终于被定义出来了。
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
2、其他属性的解析
parseBeanDefinitionElement方法是生成这个AbstractBeanDefinition 的地方:
1 | //详细对<Bean>元素中配置的Bean定义其他属性进行解析,由于上面的方法中已经对Bean的id、name和别名等属性进行了处理,该方法中主要处理除这三个以外的其他属性数据 |
这里是BeanDefinition生成的地方,大部分属性都被解析出来,下面再次以<property>
属性为例,分析一下这个属性的解析过程。
3、property元素的解析
在解析过程中,这些属性值会被封装成PropertyValue对象并设置到BeanDefinition中去,源代码:
1 | //解析<Bean>元素中的<property>子元素 |
可以看出,对<property>
解析的时候,先根据
- 先根据propertyName 来确定
<property>
的名字,然后封装一个PropertyEntry对象 - 如果已经存在一个同名的则直接返回。可知:如果在同一个Bean中配置同名的property,则只有第一个起作用
- 如果没有同名的,则继续解析property的值 ,并将其封装成PropertyValue对象
- 解析
<property>
的所有子元素,只能是其中一种类型:ref,value,list等,- 对值的属性进行判断,是ref还是value,不能同时是这两者
- 如果属性是ref,创建一个ref的数据对象RuntimeBeanReference,这个对象封装了ref信息
- 如果属性是value,创建一个value的数据对象TypedStringValue,这个对象封装了value信息
- ref和value都通过
ref(或valueHolder).setSource(extractSource(ele));
方法将属性值/引用与所引用的属性关联起来。 - 不是ref或value而是子元素,则继续通过parsePropertySubElement方法解析
4、property子元素的解析
下面是对property子元素的解析过程,Array、List、Set、Map等元素都会在这里进行解析:
1 | //解析<property>元素中ref,value或者集合等子元素 |
这个方法很明白,就是根据子元素可能的属性进行判断,然后分别调用对应的方法进行解析。
下面以list的解析为例:
1 | //解析<list>集合子元素 |
可以看出,这个方法返回的是一个List对象,而具体的类是Spring定义的ManagedList,作为封装List这类配置定义的数据封装。
这应该是最后一层了,至此,Spring IoC现在已经将XML形式定义的Bean定义资源文件转换为Spring IoC所识别的数据结构——BeanDefinition,它是Bean定义资源文件中配置的POJO对象在Spring IoC容器中的映射,我们可以通过AbstractBeanDefinition为入口,让IoC容器进行索引、查询和操作。
通过Spring IoC容器对Bean定义资源的解析后,IoC容器大致完成了管理Bean对象的准备工作,即初始化过程,但是最为重要的依赖注入还没有发生,现在在IoC容器中BeanDefinition存储的只是一些静态信息,接下来需要向容器注册Bean定义信息才能全部完成IoC容器的初始化过程
4、BeanDefinition的注册
在3.2的DefaultBeanDefinitionDocumentReader类中提到,在完成解析之后,就会将这个BeanDefinitionHolder对象作为参数调用BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())方法,来启动注册的过程。
下面来看一下这个registerBeanDefinition方法
1 | //将解析的BeanDefinitionHold注册到容器中 |
当调用BeanDefinitionReaderUtils向IoC容器注册解析的BeanDefinition时,真正完成注册功能的是DefaultListableBeanFactory。在DefaultListableBeanFactory中,是通过一个ConcurrentHashMap来持有这个BeanDefinition的,
1 | /** Map of bean definition objects, keyed by bean name */ |
4.1、DefaultListableBeanFactory
下面就是具体的注册过程
1 | //存储注册的俄BeanDefinition |
至此,Bean定义资源文件中配置的Bean被解析过后,已经注册到IoC容器中,被容器管理起来,真正完成了IoC容器初始化所做的全部工作。现在IoC容器中已经建立了整个Bean的配置信息,这些BeanDefinition信息已经可以使用,并且可以被检索,IoC容器的作用就是对这些注册的Bean定义信息进行处理和维护。这些的注册的Bean定义信息是IoC容器控制反转的基础,正是有了这些注册的数据,容器才可以进行依赖注入。
三、总结
这篇源码分析应该是我用时最长的一篇,因为实在是太复杂了,写到这里最上面的已经有些记不清了,不知道再过几天还能记住多少,不过确实收获了很多东西
- 开始最难的就是各个类定义的比较多,找不到对应的关系,但是弄明白以后发现这样多层的继承关系有着很强的可扩展性,学到了一个设计模式就是模版模式,在父类中定义好整体流程,具体的方法留在各个不同的子类中实现,各种不同资源的定位就是用的这个思想
- 载入和解析过程主要就是XML文件的解析,这里又学习了Java中XML文件的解析方法,DOM和SAX两种方法的区别,Spring中用的是JAXP的方法,而不是很有名的JDOM或者DOM4J。
- 其他就是明白了整个bean从定义,到变成容器中数据结构的整体流程
- 学习是一个厚积薄发的过程,开始的时候先不要看一些超出自己能力太多的东西,比如从刚开学的时候就想学Spring的源码,看了Spring技术内幕和很多博客,但是怎么看都看不懂。过了半个学期以后基本能勉强看懂了,当然,和Spring技术内幕这本书排版太乱也有一定关系
- 主要参考的还是http://www.cnblogs.com/ITtangtang/p/3978349.html这篇博客,感谢作者。
四、参考地址
http://blog.csdn.net/randyjiawenjie/article/details/8315861
http://elim.iteye.com/blog/2016305
http://www.blogjava.net/DLevin/archive/2012/12/01/392337.html
http://www.cnblogs.com/chenssy/p/5619170.html