注1:Spring源码基于Spring3.1版本
注2:参考《Spring技术内幕》第二版
Spring IoC容器的初始化包括BeanDefinition的Resource定位、载入和注册三个基本过程。在具体分析这三个过程之前,需要注意的是Spring把这三个过程分开,并使用不同的模块来完成,这样使Spring IoC容器更加灵活。
这里Resource指的是BeanDefinition的资源定位,它由ResourceLoader通过统一的Resource接口来完成,对各种形式的BeanDefinition都提供了统一的接口。比如文件系统中的Bean定义信息可以使用FileSystemResource来抽象;类路径中的Bean定义信息可以使用ClassPathResource来抽象,等等。
这个Resource定位过程其实就是IoC容器寻找Bean定义数据的过程。
关于Spring中Resource的更多介绍,这里有一篇博客:http://blog.csdn.net/zhangyihui1986/article/details/8793626
这个载入过程就是把定义好的Bean表示成容器内部的数据结构BeanDefinition。这个BeanDefinition实际上就是POJO对象在IoC容器中的抽象,通过这个BeanDefinition定义的数据结构,使IoC容器能够方便地对POJO对象也就是Bean进行管理。
这个过程是通过调用BeanDefinitionRegistry接口的实现来完成的,该注册过程把载入过程中解析得到的BeanDefinition向IoC容器进行注册。其实在IoC容器内部就是将BeanDefinition注入到一个HashMap中,IoC容器就是通过这个HashMap来持有这些BeanDefinition数据的。
需要注意的是,这里说的IoC容器的初始化过程一般不包括依赖注入的实现。在Spring的设计中,Bean定义的载入和依赖注入是两个独立的过程,依赖注入一般发生在应用第一次通过getBean向容器索取Bean实例的时候。有一个例外就是当我们在Bean定义中指定的lazyinit属性(预实例化配置),那么这个Bean的依赖注入在IoC容器的初始化时就完成了,不需要等到初始化完成之后,第一次获取该Bean时才触发。
下面从源码分析一下IoC容器初始化过程中的第一个步骤 -- Resource定位。
源码分析从FileSystemXmlApplicationContext类开始,因为《Spring技术内幕》中就是以该类为为起点来讲述IoC容器的初始化,这样可以引用书中的一些理论及代码解释。
如果我们以编程的方式使用DefaultListableBeanFactory时,会使用类似下面的方式:
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); Resource resource = new FileSystemResource("xxx.xml"); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(resource);
我们需要创建一个FileSystemResource(或其它Resource如ClassPathResource)来定位Resource,这里的Resource并不能为DefaultListableBeanFactory直接使用,而是通过BeanDefinitionReader(上例中使用实现类XmlBeanDefinitionReader)对这些信息进行处理。
而使用ApplicationContext相对于直接使用DefaultListableBeanFactory带来的一点好处就是ApplicationContext已经提供了一系列加载不同的Resource的功能,因为Spring中的各种ApplicationContext都实现了ResourceLoader接口(基类AbstractApplicationContext继承了DefaultResourceLoader,而DefaultResourceLoader是ResourceLoader接口的默认实现类)。
先分别从Spring源码角度和UML类图角度看一下FileSystemXmlApplicationContext的继承体系:
从继承体系可以看出,FileSystemXmlApplicationContext已经通过继承AbstractApplicationContext具备了ResourceLoader读入以Resource形式定义的BeanDefinition的能力,因为AbstractApplicationContext的基类是DefaultResourceLoader,而DefaultResourceLoader是ResourceLoader的默认实现。
FileSystemXmlApplicationContext源码实现:
package org.springframework.context.support; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext { public FileSystemXmlApplicationContext() {} public FileSystemXmlApplicationContext(ApplicationContext parent) { super(parent); } // configLocation是BeanDefinition所在的文件路径 public FileSystemXmlApplicationContext(String configLocation) throws BeansException { this(new String[] {configLocation}, true, null); } // 可以指定多个BeanDefinition资源路径 public FileSystemXmlApplicationContext(String... configLocations) throws BeansException { this(configLocations, true, null); } // 可以指定多个BeanDefinition资源路径, 同时指定自己的双亲容器 public FileSystemXmlApplicationContext(String[] configLocations, ApplicationContext parent) throws BeansException { this(configLocations, true, parent); } public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { this(configLocations, refresh, null); } // 调用父类AbstractRefreshableConfigApplicationContext的方法,设置BeanDefinition定义的资源文件,完成IoC容器Bean定义资源的定位 // 调用父类AbstractApplicationContext的refresh()方法, 这个方法启动了BeanDefinition的载入过程, 是Ioc容器载入Bean定义的入口 public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { super(parent); setConfigLocations(configLocations); if (refresh) { refresh(); } } // 覆盖父类DefaultResourceLoader的方法,通过FileSystemResource得到在文件系统中定位的BeanDefinition // 该getResourceByPath方法在BeanDefinitionReader的loadBeanDefinition中被调用, loadBeanDefinition采用模版模式, 具体定位由子类完成 protected Resource getResourceByPath(String path) { if (path != null && path.startsWith("/")) { path = path.substring(1); } return new FileSystemResource(path); } }
可以看到,在构造函数中实现了对configuration进行处理的操作,然后调用refresh方法启动IoC容器的初始化,这个refresh方法定义在AbstractApplicationContext类中,是一个非常重要的方法,是分析容器初始化过程的重要入口。
下面我们来看一下refresh方法到getResourceByPath方法的调用关系图:
由上图可以清楚地看到整个BeanDefinition资源定位的过程,最初是由refresh方法触发,而refresh的调用是在FileSystemXmlApplicationContext的构造函数中启动。如果跟着源码去refresh方法中查看,就会发现refresh方法好像是一个总开关,在方法内部调用了很多其它的方法来完成IoC的初始化,其中就有obtainFreshBeanFactory方法,而obtainFreshBeanFactory又调用了AbstractRefreshableApplicationContext类中的refreshBeanFactory方法,正是由这个方法调用loadBeanDefinitions来加载BeanDefinition,下面来看一下refreshBeanFactory方法的实现。
由上面分析可知,AbstractApplicationContext类的refresh方法是IoC容器初始化的入口,refresh方法调用了很多其它的方法,其实中就有obtainFreshBeanFactory方法,而obtainFreshBeanFactory又调用了AbstractRefreshableApplicationContext类中的refreshBeanFactory方法,我们从refreshBeanFactory方法开始分析。
AbstractRefreshableApplicationContext类中的refreshBeanFactory方法:
protected final void refreshBeanFactory() throws BeansException { // 如果已经建立了BeanFactory,则销毁并关闭该BeanFactory if (hasBeanFactory()) { destroyBeans(); closeBeanFactory(); } try { // 创建并持有DefaultListableBeanFactory实例,同时调用loadBeanDefinitions方法来载入BeanDefinition信息 DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); customizeBeanFactory(beanFactory); loadBeanDefinitions(beanFactory); synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); } } // 创建DefaultListableBeanFactory实例,getInternalParentBeanFactory方法的实现可以参看AbstractApplicationContext类源码,它会根据已有的双亲IoC容器信息来生成DefaultListableBeanFactory的双亲容器 protected DefaultListableBeanFactory createBeanFactory() { return new DefaultListableBeanFactory(getInternalParentBeanFactory()); }
接着进入到loadBeanDefinitions方法,这个方法在AbstractApplicationContext中是个抽象方法,具体的实现在AbstractXmlApplicationContext类中。
AbstractXmlApplicationContext的loadBeanDefinitions方法:
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { // 使用刚创建的BeanFactory创建一个新的XmlBeanDefinitionReader. XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); // 配置XmlBeanDefinitionReader. beanDefinitionReader.setEnvironment(this.getEnvironment()); beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); // 初始化XmlBeanDefinitionReader, 然后加载BeanDefinition initBeanDefinitionReader(beanDefinitionReader); loadBeanDefinitions(beanDefinitionReader); } // 实际加载BeanDefinition protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { Resource[] configResources = getConfigResources(); if (configResources != null) { reader.loadBeanDefinitions(configResources); } String[] configLocations = getConfigLocations(); if (configLocations != null) { reader.loadBeanDefinitions(configLocations); } }
从上面一段代码可以得知,loadBeanDefinitions方法首先创建了一个XmlBeanDefinitionReader实例,在完成配置与初始化之后执行了该XmlBeanDefinitionReader的loadBeanDefinitions方法,这里是真正载入BeanDefinition的地方,自此程序执行流程进入到XmlBeanDefinitionReader中。
跟着去看一下如何完成的。
XmlBeanDefinitionReader的loadBeanDefinitions方法:
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { int counter = 0; for (String location : locations) { counter += loadBeanDefinitions(location); } return counter; } public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException { return loadBeanDefinitions(location, null); } public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException { // 取得ResourceLoader, 这里使用的是DefaultResourceLoader, AbstractApplicationContext继承了DefaultResourceLoader ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader == null) { throw new BeanDefinitionStoreException("Cannot import bean definitions from location [" + location + "]: no ResourceLoader available"); } // 对Resource的路径模式进行解析, 得到需要的Resource集合, 这些Resource集合指向我们定义的BeanDefinition信息, 可以是多个文件 if (resourceLoader instanceof ResourcePatternResolver) { // 调用DefaultResourceLoader的getResources方法完成具体的Resource定位 try { Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); int loadCount = loadBeanDefinitions(resources); if (actualResources != null) { for (Resource resource : resources) { actualResources.add(resource); } } return loadCount; } catch (IOException ex) { throw new BeanDefinitionStoreException("Could not resolve bean definition resource pattern [" + location + "]", ex); } } else { // 调用DefaultResourceLoader的getResource方法完成具体的Resource定位. Resource resource = resourceLoader.getResource(location); int loadCount = loadBeanDefinitions(resource); if (actualResources != null) { actualResources.add(resource); } return loadCount; } }
由上面代码可以看出,多个资源定位符location会被逐一定位加载;另外,对于FileSystemXmlApplicationContext会进入loadBeanDefinitions方法中的else流程,这里会直接调用ResourceLoader的getResource方法,也就是其默认实现类DefaultResourceLoader的getResource方法。
DefaultResourceLoader的getResource方法:
// 具体取得Resource的过程, 定义在DefaultResourceLoader中 public Resource getResource(String location) { // 处理带有classpath标识的Resource if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // 作为URL定位的Resource处理 URL url = new URL(location); return new UrlResource(url); } catch (MalformedURLException ex) { // 不是URL标识的location, 将Resource定位任务交给getResourceByPath, // 这是个protected方法, 默认实现为得到一个ClassPathContextResource, 常用来被子类重写. return getResourceByPath(location); } } }
在前面的源码分析中,getResourceByPath方法被DefaultResourceLoader的子类FileSystemXmlApplicationContext实现,这个方法返回一个FileSystemResource对象,通过该FileSystemResource对象,Spring便可以完成BeanDefinition定位。
分析到这里IoC容器FileSystemXmlApplicationContext的Resource的定位已经一目了然了,如果是其它类型的ApplicationContext,那么会相应生成其它各类的Resource,比如ClassPathResource、ServletContextResource等。