Spring源码分析(四)容器的基础XmlBeanFactory
摘要:本文结合《Spring源码深度解析》来分析Spring 5.0.6版本的源代码。若有描述错误之处,欢迎指正。
经过Spring源码分析(二)容器基本用法和Spring源码分析(三)容器核心类两篇文章,我们已经对Spring的容器功能有了一个大致的了解,尽管你可能还很迷糊,但是不要紧,接下来我们会详细探索每个步骤的实现。首先要深入分析的是以下功能的代码实现:
- BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring/spring-test.xml"));
通过XmlBeanFactory初始化时序图,我们看下上面代码的执行逻辑:
时序图从BeanFactoryTest测试类开始,通过时序图我们可以一目了然地看到整个逻辑处理顺序。先调用了ClassPathResource的构造函数来构造Resource资源文件的实例对象,后续的资源处理就可以用Resource提供的各种服务来操作了,当我们有了Resource后就可以进行XmlBeanFactory的初始化了。那么Resource文件是如何封装的呢?
1. 配置文件封装
Spring的配置文件读取是通过ClassPathResource进行封装的,如new ClassPathResource("spring/spring-test.xml"),那么ClassPathResource完成了什么功能呢?
在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHander)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同的前缀(协议,Protocol)来识别,如“file:”、"http:"、"jar:"等,然而URL没有默认定义相对Classpath或ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如“classpath:”,然而这需要了解URL的实现机制,而且URL也没有提供一些基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。因而Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源。
- public interface InputStreamSource {
- InputStream getInputStream() throws IOException;
- }
- public interface Resource extends InputStreamSource {
- boolean exists();
- default boolean isReadable() {
- return true;
- }
- default boolean isOpen() {
- return false;
- }
- default boolean isFile() {
- return false;
- }
- URL getURL() throws IOException;
- URI getURI() throws IOException;
- File getFile() throws IOException;
- default ReadableByteChannel readableChannel() throws IOException {
- return Channels.newChannel(getInputStream());
- }
- long contentLength() throws IOException;
- long lastModified() throws IOException;
- Resource createRelative(String relativePath) throws IOException;
- @Nullable
- String getFilename();
- String getDescription();
- }
InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等。它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象。
Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带文件信息的文件名,getFilename())的方法。为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative()。在错误处理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用于在错误处理中的打印信息。
对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClasspathResource)、URL资源(URLResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。相关类图如下图所示:
在日常的开发工作中,资源文件的加载也是经常用到的,可以直接使用Spring提供的类,比如在希望加载文件时可以使用以下代码:
- Resource resource = new ClassPathResource("spring/spring-test.xml.xml");
- InputStream inputStream = resource.getInputStream();
得到inputStream后,我们可以按照以前的开发方式进行实现了,并且我们已经可以利用Resource及其子类为我们提供好的诸多特性。
有了Resource接口便可以对所有资源文件进行统一处理。至于实现,其实是非常简单的,以getInputStream为例,ClassPathResource中的实现方式便是通过class或者classLoader提供的底层方法进行调用,而对于FileSystemResource的实现其实更简单,直接使用FileInputStream对文件进行实例化。
ClassPathResource.java
- /**
- * This implementation opens an InputStream for the given class path resource.
- * @see java.lang.ClassLoader#getResourceAsStream(String)
- * @see java.lang.Class#getResourceAsStream(String)
- */
- @Override
- public InputStream getInputStream() throws IOException {
- InputStream is;
- if (this.clazz != null) {
- is = this.clazz.getResourceAsStream(this.path);
- }
- else if (this.classLoader != null) {
- is = this.classLoader.getResourceAsStream(this.path);
- }
- else {
- is = ClassLoader.getSystemResourceAsStream(this.path);
- }
- if (is == null) {
- throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
- }
- return is;
- }
FileSystemResource.java
- /**
- * This implementation opens a NIO file stream for the underlying file.
- * @see java.io.FileInputStream
- */
- @Override
- public InputStream getInputStream() throws IOException {
- try {
- return Files.newInputStream(this.file.toPath());
- }
- catch (NoSuchFileException ex) {
- throw new FileNotFoundException(ex.getMessage());
- }
- }
当通过Resource相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了。
了解了Spring中将配置文件封装为Resource类型的实例方法后,我们就可以继续探讨XmlBeanFactory的初始化方法了,XmlBeanFactory初始化有许多方法,Spring中提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的方法,代码如下:
- /**
- * Create a new XmlBeanFactory with the given resource,
- * which must be parsable using DOM.
- * @param resource the XML resource to load bean definitions from
- * @throws BeansException in case of loading or parsing errors
- */
- public XmlBeanFactory(Resource resource) throws BeansException {
- // 调用XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory)构造方法
- this(resource, null);
- }
- /**
- * Create a new XmlBeanFactory with the given input stream,
- * which must be parsable using DOM.
- * @param resource the XML resource to load bean definitions from
- * @param parentBeanFactory parent bean factory
- * @throws BeansException in case of loading or parsing errors
- */
- // parentBeanFactory为父类BeanFactory用于factory合并,可以为空
- public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
- super(parentBeanFactory);
- this.reader.loadBeanDefinitions(resource);
- }
上面函数的代码中,this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现,也是我们分析的重点之一。我们可以看到时序图中提到的XmlBeanDefinitionReader加载数据就是这里完成的,但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),跟踪代码到父类AbstractAutowireCapableBeanfactory的构造函数中:
- /**
- * Create a new AbstractAutowireCapableBeanFactory.
- */
- public AbstractAutowireCapableBeanFactory() {
- super();
- ignoreDependencyInterface(BeanNameAware.class);
- ignoreDependencyInterface(BeanFactoryAware.class);
- ignoreDependencyInterface(BeanClassLoaderAware.class);
- }
这里有必要提及下ignoreDependencyInterface方法。ignoreDependencyInterface的主要功能,是忽略给定接口的自动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?
举例来说,当A中有属性B,那么当Spring在获取A的Bean的如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring提供的一个重要特性。但是,某些情况下B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。Spring中是这样介绍的,自动装配的时候,忽略给定的依赖接口,典型的应用是通过其他方式解析Application上下文注册依赖,类似于BeanFactory通过BeanFactoryAware进行注入或者ApplicationContext通过ApplicationContextAware进行注入。
2. 加载Bean
之前提到的在XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,我们先来看看这个方法的时序图,如下图所示:
看到上图我们才知道,原来绕了这么久还没有切入正题,还一直在为加载XML文件和解析注册Bean在做准备工作。从上面的时序图中我们尝试梳理整个的处理过程如下:
- 封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodeResource类进行封装。
- 获取输入流。从Resource中获取对应的InputStream并构造InputSource。
- 通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions。
我们来看一下loadBeanDefinitions函数具体的实现过程:
- /**
- * Load bean definitions from the specified XML file.
- * @param resource the resource descriptor for the XML file
- * @return the number of bean definitions found
- * @throws BeanDefinitionStoreException in case of loading or parsing errors
- */
- @Override
- public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
- return loadBeanDefinitions(new EncodedResource(resource));
- }
那么EncodeResource的作用是什么呢?通过名称,我们可以大致推断这个类主要是用于对资源文件的编码进行处理。其中的主要逻辑体现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码。
- /**
- * Open a {@code java.io.Reader} for the specified resource, using the specified
- * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
- * (if any).
- * @throws IOException if opening the Reader failed
- * @see #requiresReader()
- * @see #getInputStream()
- */
- public Reader getReader() throws IOException {
- if (this.charset != null) {
- return new InputStreamReader(this.resource.getInputStream(), this.charset);
- }
- else if (this.encoding != null) {
- return new InputStreamReader(this.resource.getInputStream(), this.encoding);
- }
- else {
- return new InputStreamReader(this.resource.getInputStream());
- }
- }
上面代码构造了一个有编码(encoding)的InputStreamReader。当构造好encodeResource对象后,再次转入了可复用方法loadBeanDefinitions(new EncodedResource(resource))。
这个方法内部才是真正的数据准备阶段,也就是时序图锁描述的逻辑:
- public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
- Assert.notNull(encodedResource, "EncodedResource must not be null");
- if (logger.isDebugEnabled()) {
- logger.debug("Loading XML bean definitions from " + encodedResource.getResource());
- }
- // 通过属性来记录已经加载的资源
- Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
- if (currentResources == null) {
- currentResources = new HashSet<>(4);
- this.resourcesCurrentlyBeingLoaded.set(currentResources);
- }
- if (!currentResources.add(encodedResource)) {
- throw new BeanDefinitionStoreException(
- "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
- }
- try {
- // 从encodedResource中获取已经封装的Resource对象并再次从Resource中获取其中的inputStream
- InputStream inputStream = encodedResource.getResource().getInputStream();
- try {
- // InputSource这个类并不是来自于Spring,他的全路径是org.xml.sax.InputSource
- InputSource inputSource = new InputSource(inputStream);
- if (encodedResource.getEncoding() != null) {
- inputSource.setEncoding(encodedResource.getEncoding());
- }
- // 真正进入了逻辑核心部分
- return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
- }
- finally {
- // 关闭输入流
- inputStream.close();
- }
- }
- catch (IOException ex) {
- throw new BeanDefinitionStoreException(
- "IOException parsing XML document from " + encodedResource.getResource(), ex);
- }
- finally {
- currentResources.remove(encodedResource);
- if (currentResources.isEmpty()) {
- this.resourcesCurrentlyBeingLoaded.remove();
- }
- }
- }
我们再次准备一下数据准备阶段的逻辑,首先对传入的resource参数做封装,目的是考虑到Resource可能存在编码要求的情况,其次,通过SAX读取XML文件的方式来准备InputSource对象,最后将准备的数据通过参数传入真正的核心处理部分doLoadBeanDefinitions(inputSource, encodedResource.getResource())。
- protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
- throws BeanDefinitionStoreException {
- try {
- Document doc = doLoadDocument(inputSource, resource);
- return registerBeanDefinitions(doc, resource);
- }
- catch (BeanDefinitionStoreException ex) {
- throw ex;
- }
- catch (SAXParseException ex) {
- throw new XmlBeanDefinitionStoreException(resource.getDescription(),
- "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
- }
- catch (SAXException ex) {
- throw new XmlBeanDefinitionStoreException(resource.getDescription(),
- "XML document from " + resource + " is invalid", ex);
- }
- catch (ParserConfigurationException ex) {
- throw new BeanDefinitionStoreException(resource.getDescription(),
- "Parser configuration exception parsing XML from " + resource, ex);
- }
- catch (IOException ex) {
- throw new BeanDefinitionStoreException(resource.getDescription(),
- "IOException parsing XML document from " + resource, ex);
- }
- catch (Throwable ex) {
- throw new BeanDefinitionStoreException(resource.getDescription(),
- "Unexpected exception parsing XML document from " + resource, ex);
- }
- }
上面的代码只做了两件事,每一件都是必不可少的。
- 加载XML文件,并得到对应的Document。
- 根据返回的Document注册Bean信息。
这两个步骤支撑着整个Spring容器部分的实现基础,尤其是第二部对配置文件的解析,逻辑非常复杂,下一节里面先从获取Document讲起。
Spring源码分析(四)容器的基础XmlBeanFactory的更多相关文章
- SPRING源码分析:IOC容器
在Spring中,最基本的IOC容器接口是BeanFactory - 这个接口为具体的IOC容器的实现作了最基本的功能规定 - 不管怎么着,作为IOC容器,这些接口你必须要满足应用程序的最基本要求: ...
- 【spring源码分析】IOC容器初始化(四)
前言:在[spring源码分析]IOC容器初始化(三)中已经分析了BeanDefinition注册之前的一些准备工作,下面将进入BeanDefinition注册的核心流程. //DefaultBean ...
- 【spring源码分析】IOC容器初始化(总结)
前言:在经过前面十二篇文章的分析,对bean的加载流程大致梳理清楚了.因为内容过多,因此需要进行一个小总结. 经过前面十二篇文章的漫长分析,终于将xml配置文件中的bean,转换成我们实际所需要的真正 ...
- 【spring源码分析】IOC容器初始化(一)
前言:spring主要就是对bean进行管理,因此IOC容器的初始化过程非常重要,搞清楚其原理不管在实际生产或面试过程中都十分的有用.在[spring源码分析]准备工作中已经搭建好spring的环境, ...
- 【spring源码分析】IOC容器初始化(三)
前言:在[spring源码分析]IOC容器初始化(二)中已经得到了XML配置文件的Document实例,下面分析bean的注册过程. XmlBeanDefinitionReader#registerB ...
- Spring源码分析专题 —— IOC容器启动过程(上篇)
声明 1.建议先阅读<Spring源码分析专题 -- 阅读指引> 2.强烈建议阅读过程中要参照调用过程图,每篇都有其对应的调用过程图 3.写文不易,转载请标明出处 前言 关于 IOC 容器 ...
- 【spring源码分析】IOC容器初始化——查漏补缺(二)
前言:在[spring源码分析]IOC容器初始化(八)中多次提到了前置处理与后置处理,本篇文章针对此问题进行分析.Spring对前置处理或后置处理主要通过BeanPostProcessor进行实现. ...
- Spring Ioc源码分析系列--Ioc的基础知识准备
Spring Ioc源码分析系列--Ioc的基础知识准备 本系列文章代码基于Spring Framework 5.2.x Ioc的概念 在Spring里,Ioc的定义为The IoC Containe ...
- 【spring源码分析】IOC容器初始化(二)
前言:在[spring源码分析]IOC容器初始化(一)文末中已经提出loadBeanDefinitions(DefaultListableBeanFactory)的重要性,本文将以此为切入点继续分析. ...
随机推荐
- CentOS7系列--3.1CentOS7中配置NFS服务
CentOS7配置NFS服务 1. 配置NFS服务器端 1.1. 安装nfs-utils软件 [root@server1 ~]# yum install -y nfs-utils Loaded plu ...
- Ubuntu 添加删除用户 How to Add and Delete Users on Ubuntu 16.04
Introduction One of the most basic tasks that you should know how to do on a fresh Linux server is ...
- mvp 在 flutter 中的应用
在 Android 应用程序开发过程中,我们经常会用到一些所谓的架构方法,如:mvp,mvvm,clean等.之所以这些方法会被推崇是因为他们可以大大的解耦我们的代码的功能模块,让我们的代码在项目中后 ...
- zookeeper应用 - leader选举 锁
模拟leader选举: 1.zookeeper服务器上有一个/leader节点 2.在/leader节点下创建短暂顺序节点/leader/lock-xxxxxxx 3.获取/leader的所有子节点并 ...
- dbms_random 包的使用
dbms_random是一个可以生成随机数值或者字符串的程序包. 这个包有 initialize(),seed(),terminate(),value(),normal(),random(),stri ...
- redis介绍(5)主从复制
redis的主从复制: 主从复制介绍:redis的主从复制情况下,一个master节点下可以有多个slave节点,而且每个slave节点又可以有很多slave节点,形成很大的集群量级,我简单画个图,如 ...
- Week3——书上的分析
1.long before=System.currentTimeMills(); long after=System.currentTimeMills(); 该l两句是分别记录了开始过滤和结 ...
- eclipse工具的安装配置
安装环境 系统:Windows7 软件:jre-8u73-windows-x64.exe,eclipse-inst-win64.exe Eclipse的安装过程 1.安装jre-8u73-window ...
- Mysql性能监控项及sql语句
推荐一款mysql监控软件MONyog 1.查询缓存: mysql> show variables like '%query_cache%'; 2.缓存在Cache中线程数量thread_cac ...
- Mac .DS_Store 隐藏文件和清理.DS_Store的方法
1.清理.DS_Store的方法 输入命令: sudo find / -name ".DS_Store" -depth -exec rm {} \; 2.设置不产生.DS_Stor ...