Spring IoC 源码解析:简单容器的初始化过程
本文将主要对定义在 XML 文件中的 bean 从静态配置到加载成为可使用对象的过程,即 IoC 容器的初始化过程进行一个整体的分析。在讲解上不主张对各个组件进行深究,只求对简单容器的实现有一个整体的认识,具体实现细节留到后面专门用针对性的篇章进行讲解。
首先我们引入一个 Spring 入门示例,假设我们现在定义了一个类 MyBean,我们希望利用 Spring 管理类对象。这里我们采用 Spring 经典的 XML 配置文件形式进行配置:
1 | <bean id="myBean" class="org.zhenchao.spring.ioc.MyBean"/> |
我们将配置文件命名为 spring-core.xml
,获取 bean 实例最原始的方式如下:
1 | // 1. 定义资源描述 |
上述示例虽然简单,但麻雀虽小,五脏俱全,完整的让 Spring 执行了一遍加载配置文件,创建并初始化 bean 实例的过程。虽然从 Spring 3.1 版本开始,XmlBeanFactory 已经被置为 deprecated
,但是 Spring 并没有定义出更加高级的基于 XML 加载 bean 实例的 BeanFactory,而是推荐采用更加原生的方式,即组合使用 DefaultListableBeanFactory 和 XmlBeanDefinitionReader 来完成上述过程:
1 | Resource resource = new ClassPathResource("spring-core.xml"); |
XmlBeanFactory 实际上是对 DefaultListableBeanFactory 和 XmlBeanDefinitionReader 组合使用方式的封装,并没有增加新的处理逻辑。考虑到使用习惯,我们仍将继续基于 XmlBeanFactory 分析 bean 的加载过程。
Bean 的加载过程整体上可以分成两步:
- 完成由静态配置到内存表示 BeanDefinition 的转换;
- 基于 BeanDefinition 实例创建并初始化 bean 实例。
我们将第一步称为 bean 的解析与注册的过程,解析配置并注册到容器;将第二步看作是 bean 的创建和初始化的过程。
资源的描述与加载
如上面的例子所示,在加载配置文件之前,Spring 都会将配置文件封装成 Resource 对象。Resource 本身是一个接口,是对资源描述符的一种抽象。资源(File、URL、Classpath 等等)是众多框架使用和运行的基础,Spring 当然也不例外,框架诞生之初就是基于 XML 文件对 bean 进行配置。在开始分析容器的初始化过程之前,我们先来对支撑容器运行的 Resource 接口及其实现类做一个简单的了解。
资源的抽象声明
资源在 java 中被抽象成 URL,通过注册相应的 handler 来处理不同资源的操作逻辑,而 Spring 则采用 Resource 接口对各种资源进行统一抽象。Resource 接口声明了针对资源的基本操作,包括是否存在、是否可读,以及是否已经打开等等。Resource 接口实现如下:
1 | public interface InputStreamSource { |
1 | public interface Resource extends InputStreamSource { |
由继承关系可以看到 Resource 继承了 InputStreamSource 接口,该接口描述任何可以返回 InputStream 的类,通过 InputStreamSource#getInputStream
方法获取对应的 InputStream 对象。
Resource 本身则声明了针对资源的基本操作,Spring 也针对不同类型的资源定义了相应的类实现,比如:文件(FileSystemResource)、字节数组资源(ByteArrayResource)、ClassPath 路径资源(ClassPathResource),以及 URL 资源(UrlResource)等,如下图所示(仅包含 IoC 层面的 Resource 定义):
资源的具体定义
参考上述 UML 图,可以将 Resource 的定义分为三层,其中第 1 层是 Resource 接口定义;第 2 层是对 Resource 接口的扩展,包括 AbstractResource 抽象类、WritableResource 接口,以及 ContextResource 接口;第 3 层是具体的针对不同类型资源的 Resource 实现类。
关于第 1 层 Resource 接口的定义已经在上一小节进行了说明,下面来简单介绍一下第 2 层和第 3 层中的 Resource 的定义。首先来看一下 第 2 层 ,包括:
- WritableResource
WritableResource 接口用于描述一个资源是否支持可写的特性。在 Resource 接口定义中仅描述了一个资源是否可读,因为可读相对于可写是更加基本的特性,而对于可读又可写的文件来说,可以使用 WritableResource 接口予以描述。该接口声明了 3 个方法,其中 WritableResource#isWritable
方法用于判断文件是否可写;方法 WritableResource#getOutputStream
用于获取可写文件的 OutputStream 对象;方法 WritableResource#writableChannel
用于获取可写文件的 WritableByteChannel 对象。
- ContextResource
ContextResource 是在 2.5 版本引入的一个扩展接口,用于描述从上下文环境中加载的资源,该接口仅声明了一个方法 ContextResource#getPathWithinContext
,用于获取上下文环境的相对路径。
- AbstractResource
AbstractResource 抽象类不是对某一具体资源的描述,而是一种编程技巧。Resource 接口中声明了资源的多种操作方法,如果我们直接去实现 Resource 接口,势必要提供针对每一个方法的实现,而这些方法可能并不需要全部提供支持。AbstractResource 抽象类对所有方法提供了默认实现,通过继承 AbstractResource 抽象类可以针对性的选择实现相应的方法。
下面来看一下 第 3 层 Resource 定义,这一层针对不同的资源类型定义了相应的 Resource 实现,这些实现类均派生自 AbstractResource 抽象类,其中一部分实现了 WritableResource 接口或 ContextResource 接口。
- AbstractFileResolvingResource
AbstractFileResolvingResource 抽象了解析 URL 所指代的文件为 File 对象的过程,具体的实现典型的有 UrlResource 和 ClassPathResource。AbstractFileResolvingResource 抽象类定义如下:
1 | public abstract class AbstractFileResolvingResource extends AbstractResource { |
我们来看一下 AbstractFileResolvingResource#getFile
和 AbstractFileResolvingResource#getFileForLastModifiedCheck
方法的实现:
1 | public File getFile() throws IOException { |
上述方法用于解析 URL 所指向的文件为 File 对象,首先调用 AbstractResource#getURL
方法获取 URL 对象,然后检查当前 URL 是不是 JBoss VFS 文件,如果是则走 VFS 文件解析策略,否则调用工具类方法 ResourceUtils#getFile
进行解析,过程如下:
1 | public static File getFile(URL resourceUrl, String description) throws FileNotFoundException { |
方法 AbstractFileResolvingResource#getFileForLastModifiedCheck
相对于上述方法提供了对压缩文件 URL 路径的解析,实现如下:
1 | protected File getFileForLastModifiedCheck() throws IOException { |
方法首先获取 URL 对象,然后判断是不是压缩文件 URL,如果不是就走前面的 AbstractFileResolvingResource#getFile
进行常规解析;否则,即当前 URL 的协议是 jar、war、zip、vfszip 或 wsjar 中的一个,则首先解析 URL 得到常规 URL 对象,然后执行与 AbstractFileResolvingResource#getFile
方法相同的逻辑。
针对 AbstractFileResolvingResource 主要由两个直接实现类,即 UrlResource 和 ClassPathResource。其中 UrlResource 主要是解析 file:
协议;而 ClassPathResource 主要是对类上下文环境中资源的描述,基于 ClassLoader 或 Class 来定位加载资源。
- FileSystemResource
FileSystemResource 是对文件系统类型资源的描述,这也是 Spring 中典型的资源类型。该类继承自 AbstractResource,并实现了 WritableResource 接口。
FileSystemResource 提供了两个构造方法分别由 File 对象和文件路径来构造资源对象,对于传入的路径,考虑输入的不确定性会执行 StringUtils#cleanPath
方法对其进行格式化。FileSystemResource 中的方法实现几乎都依赖于 File 类的 API。这里提一下 FileSystemResource#createRelative
方法,该方法会基于相对路径创建 FileSystemResource 对象,实现如下:
1 | public Resource createRelative(String relativePath) { |
首先利用 StringUtils#applyRelativePath
方法创建资源绝对路径,主要操作是截取 path 的最后一个文件分隔符 /
前面的内容与 relativePath 进行拼接,然后基于新的路径构造 FileSystemResource 对象。
- PathResource
PathResource 在 4.0 版本引入的基于 JDK 7 NIO 2.0 中的 Path 类所实现的资源类型。NIO 2.0 针对本地 I/O 引入了许多新的类,用来改变 java 语言在 I/O 方面一直被人诟病的慢特性,所以 PathResource 也表示 Spring 由 BIO 向 NIO 的迈进。
- DescriptiveResource
DescriptiveResource 资源并非表示一个真实可读的资源,而是对文件的一种描述,所以这类资源的 DescriptiveResource#exists
方法始终返回 false。这类资源的作用在于必要的时候用来占坑,例如文档所说的,当一个方法需要你传递一个资源对象,但又不会在方法中真正读取该对象的时候,如果没有合适的资源对象作为参数,就创建一个 DescriptiveResource 资源做参数吧。
- BeanDefinitionResource
BeanDefinitionResource 是对 BeanDefinition 对象的一个包装。上一篇我们曾介绍过 BeanDefinition 对象是 Spring 核心类之一,是对 bean 定义在 IoC 容器内部进行表示的数据结构,我们在配置文件中定义的 bean,经过加载之后都会以 BeanDefinition 对象的形式存储在 IoC 容器中。BeanDefinitionResource 在实现上仅仅是持有 BeanDefinition 对象,并提供 getter 方法,而一般资源操作方法几乎都不支持。
- ByteArrayResource
ByteArrayResource 利用字节数组作为资源存储的标的,JDK 原生也提供了字节数组式的 I/O 流,所以二者在设计思想是相通的。
- VfsResource
VfsResource 对 JBoss Virtual File System (VFS) 提供了支持,针对 JBoss VFS 的说明,官网简介如下:
The Virtual File System (VFS) framework is an abstraction layer designed to simplify the programmatic access to file system resources. One of the key benefits of VFS is to hide certain file system details and allow for file system layouts that are not required to reflect a real file system. This allows for great flexibility and makes it possible to navigate arbitrary structures (ex. archives) as though they are part of a single file system.
具体没用过,不多做解释。
- InputStreamResource
InputStreamResource 基于给定的 InputStream 来创建资源,流是一般文件的更低一层,程序设计的共性就是越往底层走需要考虑的问题就越多,所以 Spring 明确表示,如果有相应的上层实现则不推荐直接使用 InputStreamResource。
资源加载
Spring 定义了 ResourceLoader 接口用于抽象对于资源的加载操作,该接口的定义如下:
1 | public interface ResourceLoader { |
其中 ResourceLoader#getResource
方法用于获取指定路径的 Resource 对象;方法 ResourceLoader#getClassLoader
则返回当前 ResourceLoader 所使用的类加载器,一些情况下我们可能需要基于该类加载器执行一些相对定位操作。
上述 UML 图展示了 ResourceLoader 的继承关系,我们可以将所有的接口分为加载器和解析器两类。加载器的作用不言而喻,对于解析器而言,由前面的分析我们知道 Spring 针对不同资源类型分别定义响应的 Resource 实现类,Spring 通过解析器解析具体资源类型,并加载返回对应的 Resource 对象。
在日常使用过程中,我们通常都是以 Ant 风格来配置资源路径。Ant 风格的支持给我们的配置带来了极大的灵活性,这也是 PathMatchingResourcePatternResolver 的功劳。路径的解析本质上依赖于各种规则,Ant 风格也不例外,有兴趣的同学可以自己阅读一下 PathMatchingResourcePatternResolver 解析路径的过程。
Bean 的解析与注册
当启动 IoC 容器时,Spring 需要读取 bean 相关的配置,并将各个 bean 的配置封装成 BeanDefinition 对象注册到容器中,上图展示了这一解析并注册过程的交互时序。当我们执行 new XmlBeanFactory(resource)
的时候已经完成了将配置文件包装成 Spring 定义的 Resource 对象,并开始执行解析和注册过程。XmlBeanFactory 的构造方法定义如下:
1 | public XmlBeanFactory(Resource resource) throws BeansException { |
构造方法首先是调用了父类 DefaultListableBeanFactory 构造方法,这是一个非常核心的类,它包含了简单 IoC 容器所具备的重要功能,是一个 IoC 容器的基本实现。然后调用了 XmlBeanDefinitionReader#loadBeanDefinitions
方法开始加载配置。
Spring 在设计上采用了许多程序设计的基本原则,比如迪米特法则、开闭原则,以及接口隔离原则等等,这样的设计为后续的扩展提供了极大的灵活性,也增强了模块的复用性。
Spring 使用了专门的 BeanDefinition 加载器对资源进行加载,这里使用的是 XmlBeanDefinitionReader 类,用来加载基于 XML 文件配置的 bean。整个加载过程可以概括如下:
- 利用 EncodedResource 二次包装 Resource 对象;
- 获取资源对应的输入流,并构造 InputSource 对象;
- 获取 XML 文件的实体解析器和验证模式,并加载 XML 文件返回 Document 对象;
- 由 Document 对象解析并注册 BeanDefinition。
上述过程执行期间,Spring 会暂存正在加载的 Resource 对象,避免在配置中出现配置文件之间的循环 import。
下面针对上述步骤展开说明。首先来看 步骤一 ,这一步会采用 EncodedResource 对 Resource 对象进行二次封装。EncodedResource 从命名来看是对于 Resource 的一种修饰,而不是用来描述某一类具体的资源,所以 EncodedResource 并没有实现 Resource 接口,而是采用了类似装饰者模式的方式对 Resource 对象进行包装,以实现对 Resource 输入流按照指定的字符集进行编码。
完成了对 Resource 对象进行编码封装之后, 步骤二 会依据编码将 Resource 对应的输入流封装成 InputSource 对象,从而为加载 XML 做准备。InputSource 并非是 Spring 中定义的类,这个类是 JDK 提供的对 XML 实体的原生支持
接下来,Spring 会调用 XmlBeanDefinitionReader#doLoadBeanDefinitions
方法正式开始针对 BeanDefinition 的加载和注册过程,对应 步骤三 和 步骤四 ,该方法实现如下:
1 | protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { |
方法逻辑还是很清晰的,第一步加载 XML 获取 Document 对象,第二步由 Document 对象解析得到 BeanDefinition 对象并注册到 IoC 容器中。
加载 XML 文件首先会获取对应的实体解析器和验证模式,方法 XmlBeanDefinitionReader#doLoadDocument
实现了获取实体解析器、验证模式,以及构造 Document 对象的逻辑:
1 | protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { |
XML 是半结构化数据,其验证模式用于保证结构的正确性,常见的验证模式有 DTD 和 XSD 两种。获取验证模式的过程实现如下:
1 | protected int getValidationModeForResource(Resource resource) { |
上述实现描述了获取验证模式的执行流程,如果没有手动指定那么 Spring 会去自动检测。对于 XML 文件的解析,SAX 首先会读取 XML 文件头声明,以获取相应验证文件地址,并下载验证文件。网络异常会影响下载过程,这个时候可以通过注册一个实体解析器实现寻找验证文件的逻辑。
完成了对于验证模式和解析器的获取,就可以开始加载 Document 对象了,这里本质上调用的是 DefaultDocumentLoader#loadDocument
方法,实现如下:
1 | public Document loadDocument(InputSource inputSource, |
整个过程与我们平常解析 XML 文件的流程大致相同。
完成了对 XML 文件到 Document 对象的构造,我们终于可以解析 Document 对象并注册 BeanDefinition 了,这一过程由 XmlBeanDefinitionReader#registerBeanDefinitions
方法实现:
1 | public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { |
上述方法所做的工作就是创建对应的 BeanDefinitionDocumentReader 对象,基于该对象加载并注册 BeanDefinition,并最终返回本次新注册的 BeanDefinition 的数量。加载并注册 BeanDefinition 的过程具体由 DefaultBeanDefinitionDocumentReader 实现:
1 | public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { |
解析的过程首先处理 <profile/>
标签,这个属性在 Spring 中不是很常用,不过在 maven 中倒是挺常见,可以类比进行理解,即在配置多套环境时可以根据部署的具体环境来选择使用哪一套配置。上述方法会先检测是否配置了 profile 标签,如果是就需要从上下文环境中确认当前激活了哪一套配置。
具体解析并注册 BeanDefinition 的过程交由 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions
方法完成,实现如下:
1 | protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { |
解析期间会判断当前标签是默认标签还是自定义标签,并按照不同的策略进行解析,这是一个复杂的过程,后面会用文章进行针对性讲解,这里暂不深究。
到这里我们已经完成了由静态配置到 BeanDefinition 的解析,并注册到 IoC 容器中的过程,下一节将继续探究如何创建并初始化 bean 实例。
Bean 实例的创建和初始化
完成了对 bean 配置的加载和解析之后,相应的配置就全部转换成 BeanDefinition 对象的形式存在于 IoC 容器中。接下来我们可以调用 AbstractBeanFactory#getBean
方法获取 bean 实例,该方法实现如下:
1 | public Object getBean(String name) throws BeansException { |
上述方法只是简单的将请求委托给 AbstractBeanFactory#doGetBean
方法进行处理,这也符合我们的预期。方法 AbstractBeanFactory#doGetBean
可以看作是是获取 bean 实例的整体框架代码,通过调度各个模块完成对 bean 实例及其依赖的 bean 实例的初始化操作,并最终返回我们期望的 bean 实例。方法实现如下:
1 | protected <T> T doGetBean(final String name, |
整个方法的执行流程可以概括为:
- 获取参数 name 对应的真正的 beanName;
- 检查缓存或者实例工厂中是否有对应的单例,若存在则进行实例化并返回对象,否则继续往下执行;
- 执行 prototype 类型依赖检查,防止循环依赖;
- 如果当前 BeanFactory 中不存在需要的 bean 实例,则尝试从父 BeanFactory 中获取;
- 将之前解析过程返得到的 GenericBeanDefinition 对象合并为 RootBeanDefinition 对象,便于后续处理;
- 如果存在依赖的 bean,则递归初始化依赖的 bean 实例;
- 依据当前 bean 的作用域对 bean 进行实例化;
- 如果对返回 bean 类型有要求则进行检查,按需做类型转换;
- 返回 bean 实例。
上述方法从整体来看就是一个框架代码,总结了从接收一个 beanName 到返回对应 bean 实例的完整流程。
总结
本文从整体的角度分析了一个 bean 从 XML 配置,到载入 IoC 容器中封装成 BeanDefinition 对象,最后依据请求初始化并返回 bean 实例的完整流程,目的在于从整体建立对 IoC 容器运行机制的认识。从下一篇开始,我们将回到起点,沿着本文梳理的 IoC 容器运行主线,对中间执行的具体细节进行深入分析。