Spring容器的基本实现

前言

在工作中的小伙伴都知道了解源码有多重要,无论是对工作还是面试。但是学习源码是困难的,早不到头绪,不知道怎么学习,而《Spring源码深度解析》这本书由浅入深,一步步解析源码,非常不错。包括本文都是参考本书来写的。

在学习Spring源码之前,咱们先创建一个测试工程,我使用的是IDEA创建的Maven工程:
在这里插入图片描述

pom 配置为

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.example</groupId>
	<artifactId>spring-sound-code</artifactId>
	<version>1.0-SNAPSHOT</version>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

然后SpringSoundCodeApplication是一个启动类,TestController是一个测试接口,启动项目,访问测试接口,正常后就可以了。

容器的基本用法

beanSpring 中最核心的东西,因为 Spring 就像是个大水桶,而 bean 就像是容器中的水,水桶脱离了水便没用处了, 那么我们先看看 bean 定义:

public class MyTestBean {
	private String testStr = "testStr";

	public void setTestStr(String testStr){
		this.testStr = testStr;
	}

	public String getTestStr(){
		return testStr;
	}
}

这只是一个简单的Bean,Spring 目的就是让我们的 bean 能成为纯粹的 POJO ,这也是 Spring 所追求的 接下来看看配置文件:
resource文件夹下新建文件 spring-bean.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean id="myTestBean" class="com.zh.sound.pojo.MyTestBean"/>
</beans>

在上面的配置中我们看到了 bean 的声明方式,尽管 Spring 中bean 的元素定义着N 种属性来支撑我们业务的各种应用,但是我们只要声明成这样,基本上就已经可以满足我们的大多数应用了. 好了,你可能觉得还有什么,但是,真没了, Spring 的人门示例到这里已经结束,我们可以写测试代码测试了。

@SpringBootTest
public class BeanFactoryTest {

	@Test
	public void testSimpleLoad(){
		XmlBeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("spring-bean.xml"));
		MyTestBean myTestBean = (MyTestBean) xmlBeanFactory.getBean("myTestBean");
		System.out.println(myTestBean.getTestStr());
	}
}

// 输出结果为:testStr

直接使用 BeanFactory 作为容器对于 Spring 的使用来说并不多见,甚至是甚少使用,因为在企级的应用中大多数都会使用的是 ApplicationContext (后续章节我们会介绍它们之间的区别),这里只是用于测试,让读者更快更好地分析 Spring 的内部原理。

接下来我们分析这个小栗子。

功能分析

现在我们可以来好好分析一下上面测试代码的功能,来探索上面的测试代码中 Spring 究竟帮助我们完成了什么工作?不管之前你是否使用过 Spring ,当然,你应该使用过的,毕竟本书面向的是对 Spring 定使用经验的读者,你都应该能猜出来,这段测试代码完成的功能无非就是以下几点。

  • 读取配置文件 spring-bean.xml
  • 根据 spring-bean.xml 中的配置找到对应的类的配置,并实例化。
  • 调用实例化后的实例。
    为了更清楚地描述,作者临时画了设计类图,如图 2-1 所示,如果想完成我们预想的功能,至少需要 3个类
    在这里插入图片描述
  • ConfigReader: 用于读取及验证配置文件。我们要用配置文件里面的东西,当然首先要做的就是读取,然后放置到内存中。
  • ReflectionUtil: 用于根据配置文件中的配置进行反射实例化。比如在上例中 spring-bean.xml 出现的 <bean id="myTestBean" class="com.zh.sound.pojo.MyTestBean"/>,我们就可以根据class的值 com.zh.sound.pojo.MyTestBean来进行实例化。
  • App: 用于完成整个逻辑的串联。

核心类介绍

DefaultlistableBeanFactory

XmlBeanFactory 继承向 DefaultListableBeanFactory ,而 DefaultListableBeanFactmy 是bean加载的核心部分,是 Spring 注册及加载 bean 的默认实现,而对于 XmlBeanFactory 与DefaultListableBeanFactory 不同的地方其实是在 XmlBeanFactory 中使用了自定义的 XML 读取器XmlBeanDefinitionReader ,实现了个性化的 BeanDefinitionReader 读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory 并实现了 ConfigurableListableBeanFactory,以及BeanDefinitionRegistry 接口。ConfigurableListableBeanFactory 。
在这里插入图片描述
相关类图
在这里插入图片描述
从上面的类图以及层次结构图中,我们可以很清晰地从全局角度了DefaultListableBeanFactory的脉络 如果读者没有了解过 Spring 源码可能对上面的类图不是很理解,不过没关系,通过后续的学习,你会逐渐了解每个类的作用。那么,让我们先简单地了解图 中各个类的作用。

  • AliasRegistry: 定义对alias 的简单增删改等操作。
  • SimpleAliasRegistry: 主要使用map作为alias的缓存,并对接口AliasRegistry进行实现。
  • SingletonBeanRegistry: 定义对单例的注册及获取。
  • BeanFactory: 定义获取 bean 及bean 的各种属性。
  • DefaultSingletonBeanRegistry: 对接口SingletonBeanRegistry各函数的实现。
  • HierarchicalBeanFactory: 继承BeanFactory, 也就是在BeanFactory 定义的功能的基础上增加了对parentFactory 的支持。
  • BeanDefinitionRegistry: 定义对BeanDefinition的各种增删改操作。
  • FactoryBeanRegistrySupport: 在DefaultSingletonBeanRegistry 基础上增加了对FactoryBean的特殊处理功能。
  • ConfigurableBeanFactory: 提供配置Factory 的各种方法。
  • ListableBeanFactory: 根据各种条件获取Bean的配置清单。
  • AbstractBeanFactory: 综合FactoryBeanRegistrySupport 和ConfigurableBeanFactory 的功能
  • AutowireCapableBeanFactory: 提供创建Bean,自动注入,初始化以及应用bean的后处理器
  • AbstractAutowireCapableBeanFactory: 综合AbstractBeanFactory 并对接口AutowireCapableBeanFactory进行实现。
  • ConfigurableListableBeanFactory: BeanFactory 配置清单,指定忽略类型及接口等。
  • DefaultListableBeanFactory: 综合上面的所有功能,主要是对bean注册后的处理。

XmlBeanFactoryDefaultListableBeanFactory 类进行了扩展,主要用于从 XML 文档中读BeanDefinition ,对于注册及获取 bean 都是使用从父类 DefaultListableBeanFactory 继承的方法去实现,而唯独与父类不同的个性化实现就是增加了 XmlBeanDefinitionReader 类型的 reader属性。 在XmlBeanFactory 中主要使用 reade 属性对资源文件进行读取和注册.

XmlBeanDefinitionReader

XML 配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置作为切入点的,那么我们可以从XmlBeanDefinitionReader中梳理一下资源文件的读取,解析及注册的大致脉络,首先我们看看XmlBeanDefinitionReader的UML类图结构.
在这里插入图片描述
各个类功能说明:

  • ResourceLoader: 定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource.
  • BeanDefinitonReader: 主要定义资源文件读取并转换为BeanDefinition 的各个功能。
  • EnvironmentCapable: 定义获取Enironment 方法。
  • DocumnetLoader: 定义从资源文件加载到转换为Document的功能
  • AbstractBeanDefinitionReader: 对EnvironmentCapable, BeanDefinitionReader类定义的功能进行实现。
  • BeanDefinitionDocumentReader: 定义读取Document 并注册BeanDefinition功能。
  • BeanDefinitionParserDelegate: 定义解析Element 的各种方法。

经过以上分析,我们可以梳理出整个XML 配置文件读取的大致流程,如上图所示,在XmlBeanDefinitionReader 中主要包含一下几步的处理。

  1. 通过继承自 AbstractBeanDefinitionReader 中的方法,来使用EResourceLoader 将资源文件路径转换为对应的Resource 文件。
  2. 通过DocumentLoader 对Resource 文件进行转换,将Resource 文件转换为Document文件。
  3. 通过实现接口BeanDefinitionDocumentReader 的 DefaultBeanDefinitionDocumentReader 类对Document 进行解析,并使用BeanDefinitionParseDelegate 对Element 进行解析。

容器的基础XmlBeanFactory

好了,到这里我们已 Spring 的容器功能有了大致的了解,尽管你可能还很迷糊,但是不要紧,接下来我们会详细探索 每个步骤的实现。 再次重申一下代码,我们接下来 深入分析以下功能的代码实现:

XmlBeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("spring-bean.xml"));

在这里插入图片描述
通过XmlBeanFacotry 初始化时序图(上图所示),我们来看 看上面代码的执行逻辑。

时序图从BeanFactoryTest 测试类开始,通过时序图我们可以一目了然的看到整个逻辑处理顺序。在测试的BeanFactoryTest 中首先调用ClassPathResource 的构造函数来构造Resource 资源的实例对象,这样后续的资源处理就可以用Resource 提供的各种服务来操作了,当我们有了Resource后就可以进行XmlBeanFactory 的初始化了。那么Resource 资源是如何封装的呐?

配置文件封装

Spring 的配置文件读取时通过ClassPathResource 进行封装的,如 new ClassPathResource("spring-bean.xml"), 那么ClassPathResource完成了什么功能呐?
在Java中,将不同来源的资源抽象成URL, 通过注册不同的handler(URLStreamHandler)来处理不同来源的资源读取逻辑,一般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 this.exists();
    }
    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(this.getInputStream());
    }
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String var1) throws IOException;
    @Nullable
    String getFilename();
    String getDescription();
}

InputStreamSource 任何能返回 InputStream 的类,比如 File, Classpath 下的资游和ByteArray等, 它只有一个方法定义 getlnputStream(),该方法返回一个新的 InputStream 对象。

Resource 接口抽 了所有 Spring 内部使用到的底层资源: File, URL, Classpath 首先,它定义了 个判断当前资源状态的方法:存在性( exists )、可读性( isReadable )、是否处于打开状态( isOpen 另外, Resomce 接口还提供了不同资源到 URL ,URI、 File 类型的转换,以及获取 lastModified 属性、文件名(不带路径信息的文件名, getFilename())的方法, 为了便于操作, Resource 还提供了基于当前资源创建一个相对资源的方法: ceateRelative()。 在错误处理中需要详细地打印出锚的资源文件,因而 Resource 还提供了 getDescription ()方法用来在错误处理中打印信息。

对不同来源的资源文件都有相应的 Resource 实现 文件( FileSystemResource),Classpath 资源( ClassPathResource )、 URL 资源( UrlResource )、 InputStream 资源( InputStreamResource),Byte 数组( ByteArrayResource )等
在这里插入图片描述
在日常的开发工作中,资源文件的加载也是经常用到的,可以直接使用 Spring 提供的类,比如在希望 加载文件时可以使用以下代码:

Resource resource = new ClassPathResource("spring-bean.xml");
InputStream inputStream = resouce.getInputStream();

得到 inputStream 后,我们就可以按照以前的开发方式进行实现了,并且我们可以利用Resource 及其子类为我们提供的诸多特性。

有了 Resource 接口便可以对所有资源文件进行统一处理 至于实现,其实是非常简单的,以getlnputStream 为例, ClassPathResource 中的实现方式便是通 class或者 classLoader 提供的底层方法进行调用,而对于 FileSystemResource 其实更简单,直接使用 FileinputStream对文件进行实例化。

ClassPathResource.java
    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(this.getDescription() + " cannot be opened because it does not exist");
        } else {
            return is;
        }
    }
 FileSystemResource.java
 public InputStream getInputStream() throws IOException {
        try {
            return Files.newInputStream(this.filePath);
        } catch (NoSuchFileException var2) {
            throw new FileNotFoundException(var2.getMessage());
        }
    }

当通过 Resource 相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader 来处理了。

了解了 Spring 将配置文件封装为 Resource 类型的实例方法后,我们就可以继续探寻XmlBeanFactory 的初始化过程了 XmlBeanFactory 的初始化有若干办法 Spring 中提供了很多的构造函数,在这里分析的是 使用Resource 实例作为构造两数参数的办法 代码如下:

 XmlBeanFactory.java
public XmlBeanFactory(Resource resource) throws BeansException {
    // 调用XmlBeanFactory(Resource, BeanFactory)构造方法
		this(resource, null);
}
// parentBeanFactory为父类BeanFactory 用于合并 factory,可以为空
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
		super(parentBeanFactory);
		this.reader.loadBeanDefinitions(resource);
	}

上面函数中的代码 this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现,也是我们分析的重点 之一,我们可以看到时序图中提到的 XmIBeanDefinitionReader 加载数据就是在这里完成的,但是在 XmlBeanDefinitionReader 加载数据前,还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中:

AbstractAutowireCapableBeanFactory.java
public AbstractAutowireCapableBeanFactory() {
		super();
		ignoreDependencyInterface(BeanNameAware.class);
		ignoreDependencyInterface(BeanFactoryAware.class);
		ignoreDependencyInterface(BeanClassLoaderAware.class);
	}

这里有必要提及 inoreDependencylnterface 方法。 ignoreDependencyInterface 的主要功能是忽略给定接口的自动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?

举例来说,当 A中有属性 ,那么 Spring 在获取 Bean 的时候如果其属性 还没有初始化,那么 Spring 会自动初始化 ,这也是 Spring 提供的一个重要特性。 但是,某些情况下, 不会被初始化,其中的一种情况就是B 实现了 BeanNameAware 接口。 Spring 中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析 Application 上下文注册依赖,类似于 BeanFactor 通过 BeanFactorAware 进行注入或者ApplicationContext 通过ApplicationContextAware 进行注入。

加载Bean

之前提到的在XmlBeanFactory 构造函数中调用了XmlBeanDefinitionReade 类型的reader 属性提供的方法 this.reader.loadBeanDefinitions(resource),而这句 代码则是整个资源加载的切入点,我们先来看看这个方法的时序图,如图所示:
在这里插入图片描述
看到上图 我们才知道什么叫山路十八弯,绕了这么半天 还没有真正地切入正题,比如加载XML 文档和解析注册 Bean 一直还在做准备工作。 我们根据上面的时序图来分析一下这里究竟在准备什么?从上面的时序图中我们尝试梳理整个的处理过程如下。

  1. 封装资源文件。当进入XmlBeanDefinitionReader 后首先对参数Resource使用EncodedResource 类进行封装。
  2. 获取输入流。从Resource 中获取对应的InputStream 并构造 InputSource.
  3. 通过构造的InputSource 实例和Resource 实例继续调用函数 doLoadBeanDefinitions

.

`doLoadBeanDefinitions` 函数具体的实现过程:
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
		return loadBeanDefinitions(new EncodedResource(resource));
	}

那么EncodedResource 的作用是什么那?通过名称,我们可以大致推断这个类主要是用于对资源文件的编码进行处理的。其中的主要逻辑体现在getReader() 方法中,当设置了编码属性的时候Spring 会使用相应的编码作为输入流的编码

 public Reader getReader() throws IOException {
        if (this.charset != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.charset);
        } else {
            return this.encoding != null ? new InputStreamReader(this.resource.getInputStream(), this.encoding) : new InputStreamReader(this.resource.getInputStream());
        }
    }

上面代码构造了一个有编码(encoding)的InputStreamReader。当构造好encodedResource对象后,再次转入了可复用方法 loadBeanDefinition(new EncodedResource(resource))
这个方法内部才是真正的数据准备阶段,也就是时序图所描述的逻辑:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
		Assert.notNull(encodedResource, "EncodedResource must not be null");
		if (logger.isTraceEnabled()) {
			logger.trace("Loading XML bean definitions from " + encodedResource);
		}
		// 通过属性来记录已经加载的资源
		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);
			int count = registerBeanDefinitions(doc, resource);
			if (logger.isDebugEnabled()) {
				logger.debug("Loaded " + count + " bean definitions from " + resource);
			}
			return count;
		}
		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);
		}
	}

上面的代码中不考虑异常类的代码,其实只做了三件事,这三件事的每一件都必不可少。

  • getValidationModeForResource(resource): 获取对XML 文件的验证模式。
  • this.documentLoader.loadDocument:加载XML文件,并得到对应的Document.
  • registerBeanDefinitions(doc, resource):根据返回的Document对象注册Bean 信息。

这3个步骤支撑着整个Spring容器部分的实现,尤其是第3部对配置文件的解析,逻辑非常复杂,我们先从获取XML 文件的验证模式讲起。

获取XML 的验证模式

了解 XML 文件的读者都应该知道 XML 文件的验证模式保证了 XML 文件的正确性,而比较常用的验证模式有两种: DTD和 XSD 它们之间有什么区别呢?

DTD 与 XSD 的区别

DTD (Document Type Definition) 即文档类型定义,是一种 XML 约束模式语言,是XML 文件的验证机制,属于XML 文件组成的一部分。DTD是一种保证XML 文档格式正确的有效方法,可以通过比较XML 文档和DTD 文件来看是否符合规范,元素和标签使用是否正确。一个DTD 文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则
要使用DTD 验证模式的时候需要在XML文件的头部声明,以下是在Spring 中使用DTD 声明方式的代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans
        PUBLIC "-//Spring//DTD BEAN 2.0//EN"
        "http://www.SPringFramework.org/dtd/Spring-beans-2.0.dtd">
	<beans>
		... ...
	</beans>

XML Schema 语言就是XSD (XML Schemas Definition)。XML Schema 描述了XML 文档的结构。可以用一个指定的XML Schema 来验证某个XML 文档,以检查该XML 文档是否符合其要求。文档设计者可以通过 XML Schema XML 档所允许的结构和内容 ,并可据此检查XML 文档是否是有效的。 XML Schema 本身是 XML 文档 它符合 XML 语法结构 可以用通用的 XML 解析器解析它。

在使用XML Schema 文档对XML 实例进行检验,除了要声明名称 间外 (xmls=http://www.Springframework.org/schema/beans ),还必须指定该名称空间所对应的 XML Schema文挡的存储位置。 通过schemaLocation 属性来指定名称空间所对应的 XML Schema 档的存储位置它包含两个部分,一部分是名称空间的 URI ,另一部分就是该名称空间所标识的 XML Schema 文件位置或 URL 地址( xsi:schemaLocation="http://www.Springframework.org/schema/beans http://www.Springframework.org/schem/beans/Spring-beans.xsd )

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    ....
</beans>
验证模式的读取

了解了 DTD与XSD 的区别后我们再去分析 Spring 中对于验证模式的提取就更容易理解了.通过之前的分析我们锁定了 Spring 通过 getValidationModeForResource 方法来获取对应资源的的验证模式。

protected int getValidationModeForResource(Resource resource) {
		int validationModeToUse = getValidationMode();
    	// 如果手动指定了验证模式,则使用指定的验证模式 
		if (validationModeToUse != VALIDATION_AUTO) {
			return validationModeToUse;
		}
    	// 为指定则使用自动检测验证模式
		int detectedMode = detectValidationMode(resource);
		if (detectedMode != VALIDATION_AUTO) {
			return detectedMode;
		}
		// Hmm, we didn't get a clear indication... Let's assume XSD,
		// since apparently no DTD declaration has been found up until
		// detection stopped (before finding the document's root tag).
		return VALIDATION_XSD;
	}

public void setValidationMode(int validationMode) {
	this.validationMode = validationMode;
}

/**
 * Return the validation mode to use.
 */
public int getValidationMode() {
	return this.validationMode;
}

方法的实现是很简单的,无非是如果设定了验证模式则使用设定的验证模式(可以通过对调用XmlBeanDefinitionReader 中的setValidationMode 方法进行设定)。否则使用自动检测的方式,而自定检测验证模式的功能是在函数 detectValidationMode 方法中实现的,在detectValidationMode 函数中又将自动检测验证模式的工作委托给了专门处理类 XmlValidationModeDetector,调用了XmlValidationModeDetectordetectValidationMode方法,具体代码如下:

XmlBeanDefinition.java
protected int detectValidationMode(Resource resource) {
		if (resource.isOpen()) {
			throw new BeanDefinitionStoreException(
					"Passed-in Resource [" + resource + "] contains an open stream: " +
					"cannot determine validation mode automatically. Either pass in a Resource " +
					"that is able to create fresh streams, or explicitly specify the validationMode " +
					"on your XmlBeanDefinitionReader instance.");
		}

		InputStream inputStream;
		try {
            // 获取resource 的输入流
			inputStream = resource.getInputStream();
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
					"Did you attempt to load directly from a SAX InputSource without specifying the " +
					"validationMode on your XmlBeanDefinitionReader instance?", ex);
		}

		try {
            // 调用XmlValidationModeDetector类的detectValidationMode 方法
			return this.validationModeDetector.detectValidationMode(inputStream);
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
					resource + "]: an error occurred whilst reading from the InputStream.", ex);
		}
	}

XmlValidationModeDetector.java
public int detectValidationMode(InputStream inputStream) throws IOException {
    	// 将输入流封装成 BufferedReader
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		// 设置返回参数
        byte var4;
        try {
            // 定义变量 是否是DTD的验证模式,默认为false
            boolean isDtdValidated = false;

            while(true) {
                String content;
                // 遍历读取数据,一次读取一行
                if ((content = reader.readLine()) != null) {
                    content = this.consumeCommentTokens(content);
                    // 读取的行如果是空或者是注释则略过
                    if (this.inComment || !StringUtils.hasText(content)) {
                        continue;
                    }
					// 如果当前行包含 DTD的声明,则结束循环,使用dtd验证模式
                    if (this.hasDoctype(content)) {
                        isDtdValidated = true;
                    } else if (!this.hasOpeningTag(content)) {
                        continue;
                    }
                }
				// 如果不是dtd, 就是用xsd 验证模式
                int var5 = isDtdValidated ? 2 : 3;
                return var5;
            }
        } catch (CharConversionException var9) {
            // 抛出异常时设置为自动,但是在外层方法自动默认使用的时xsd验证模式
            var4 = 1;
        } finally {
            reader.close();
        }

        return var4;
    }
private boolean hasDoctype(String content) {
        return content.contains("DOCTYPE");
    }
获取Document

经过了验证模式准备的步骤就可以进行Document 加载了,同样XmlBeanFacotryReader 类对于文档读取并没有亲力亲为,而是委托给了DocumentLoader 去执行,这里的DocumentLoader 是个接口,而真正给调用的是DefaultDocumentLoader,解析代码如下:

DefaultDocumentLoader.java
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

		DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
		if (logger.isTraceEnabled()) {
			logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
		}
		DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
		return builder.parse(inputSource);
	}

对于这部分代码其实并没有太多可以描述的,因为通过 SAX解析XML 文档的套路大致都差不多,Spring 在这里并没有什么特殊的地方,同样首先创建 DocumentBuilderFactory ,再通过DocumentBuilderFactory 创建DocumentBuilder ,进而解析 inputSource 来返回 Document 象。对此感兴趣的读者可以在网上获取更多的资料。 这里有必要提及一 下EntityResolver ,对于参数entityResolver ,传入的是通过EntityResolver()函数获取的返回值,如下代码:

protected EntityResolver getEntityResolver() {
		if (this.entityResolver == null) {
			// Determine default EntityResolver to use.
			ResourceLoader resourceLoader = getResourceLoader();
			if (resourceLoader != null) {
				this.entityResolver = new ResourceEntityResolver(resourceLoader);
			}
			else {
				this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
			}
		}
		return this.entityResolver;
	}
EntityResolver用法

在loadDocument 方法中涉及一个参数EntityResolver, 官网是这样解释的: 如果SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver 方法想SAX驱动器注册一个实例。也就是说,对于解析一个XML, SAX首先读取该XML文档上的声明,根据声明去寻找对应的DTD定义,一边对文档进行一个验证。默认的寻找规则,即通过网络(实际上就是声明的DTD 的URI地址)来下载相应的DTD声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有呗找打的原因。

EntityResolver 的作用时项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。

首先看entityResolver 的接口方法声明

 public abstract InputSource resolveEntity (String publicId,String systemId)
        throws SAXException, IOException;

这里,他接收两个参数 publicIdsystemId,并返回一个inputSource对象。这里我们以特定配置文件来进行讲解。

  1. 如果我们在解析验证模式为XSD 的配置文件,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	... ...
</beans>

读取到一下两个参数

  • publicId: null
  • systemId: http://www.springframework.org/schema/beans/spring-beans.xsd
  1. 如果我们在解析验证模式为DTD的配置文件,代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans
        PUBLIC "-//Spring//DTD BEAN 2.0//EN"
        "http://www.SPringFramework.org/dtd/Spring-beans-2.0.dtd">
	<beans>
		... ...
	</beans>

读取到一下两个参数

  • publicId: -//Spring//DTD BEAN 2.0//EN
  • systemId: http://www.SPringFramework.org/dtd/Spring-beans-2.0.dtd

之前已经提到过,验证文件默认的加载方式是通过URL 进行网络下载获取,这样会照成延迟,用户体验也不好,一般的做法都是将验证文件放置在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件那?我们以加载DTD 文件为例来看看Spring 中是如何实现的。根据之前Spring 中通过getEntityReolver() 方法对EntityResolver 的获取,我们知道,Spring 中使用DelegatingEntityResolver 类为EntityResolver 的实现类,resolverEntity 实现方法如下:

DelegatingEntityResolver.java
public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
    	// 构建DelegatingEntityResolver实例时,对参数dtdResolver和schemaResolver都创建对应的解析对象
		this.dtdResolver = new BeansDtdResolver();
		this.schemaResolver = new PluggableSchemaResolver(classLoader);
	}
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
			throws SAXException, IOException {

		if (systemId != null) {
            // DTD_SUFFIX = .dtd
			if (systemId.endsWith(DTD_SUFFIX)) {
                // 如果是dtd 从这里解析
				return this.dtdResolver.resolveEntity(publicId, systemId);
			}
            // XSD_SUFFIX = .xsd
			else if (systemId.endsWith(XSD_SUFFIX)) {
                // 通过调用 META-INF/Spring.schemas 解析
				return this.schemaResolver.resolveEntity(publicId, systemId);
			}
		}

		// Fall back to the parser's default behavior.
		return null;
	}

我们可以看到,对不同的验证模式,Spring 使用了不同的解析器解析。这里简单描述一下原理,比如加载DTD 类型的BeansDtdResolver 的 resolverEntity 是直接截取 systemId 最后的 xx.dtd ,然后去当前路径下寻找,而加载XSD 类型的 PluggableSchemaResplver 类的resolverEntity 是默认到META-INF/Spring.schemas 文件中找到systemId 所对应的XSD 文件并加载。

BeansDtdResolver .java
	public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Trying to resolve XML entity with public ID [" + publicId +
					"] and system ID [" + systemId + "]");
		}

		if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
			int lastPathSeparator = systemId.lastIndexOf('/');
			int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
			if (dtdNameStart != -1) {
				String dtdFile = DTD_NAME + DTD_EXTENSION;
				if (logger.isTraceEnabled()) {
					logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
				}
				try {
					Resource resource = new ClassPathResource(dtdFile, getClass());
					InputSource source = new InputSource(resource.getInputStream());
					source.setPublicId(publicId);
					source.setSystemId(systemId);
					if (logger.isTraceEnabled()) {
						logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
					}
					return source;
				}
				catch (FileNotFoundException ex) {
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
					}
				}
			}
		}

		// Fall back to the parser's default behavior.
		return null;
	}

解析及注册BeanDefinitions

当把文件转换为Document后,接下来的提取及注册Bean 就是我们的重头戏。继续上面的分析,当程序已经拥有XML 文档文件的Document 实例对象时,就会被引入下面这个方法。

XmlBeanDefinitionReader.java
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // 使用 DefaultBeanDefinitionDocumoentReader 实例化 BeanDefinitionDocumentReader
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // 在实例化BeanDefinitionReader 时候会将传入,DefinitionRegister ,默认使用继承自DefaultListableBeanFactory的子类
    // 查询统计前BeanDefinition 的加载个数
		int countBefore = getRegistry().getBeanDefinitionCount();
    // 加载及注册Bean
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // 记录本次加载的BeanDefinition 个数
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}

其中的参数doc 是通过上一节 loadDocument 加载转换出来的。在这个方法中很好的应用了卖你想对象中单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeaDefinitionDocumentReader。 BeanDefinitionDocumentReader 是一个接口,而实例化的工作就是在creatBeanDefinitionReader() 中完成的,而通过此方法,BeanDefinitionDocumentReader 真正的类型其实已经是DefaultBeanDefinitionDocumentReader 了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root, 以便于再次将root 作为参数继续 BeanDefinition的注册

DefaultBeanDefinitionDocumentReader.java
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
		this.readerContext = readerContext;
    // Document root = doc.getDocumentElement()
		doRegisterBeanDefinitions(doc.getDocumentElement());
}

经过艰难险阻,终于到了核心逻辑的底部 doRegisterBeanDefinitions(root),如果说以前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions 算是真正的开始进行解析了:

DefaultBeanDefinitionDocumentReader.java
protected void doRegisterBeanDefinitions(Element root) {
		// 专门处理解析
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);

		if (this.delegate.isDefaultNamespace(root)) {
            // 处理profile 属性
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			if (StringUtils.hasText(profileSpec)) {
				String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
						profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
				
				if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
					if (logger.isDebugEnabled()) {
						logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
								"] not matching: " + getReaderContext().getResource());
					}
					return;
				}
			}
		}
		// 解析前处理,留给子类实现
		preProcessXml(root);
		parseBeanDefinitions(root, this.delegate);
    	// 解析后处理,留给子类实现
		postProcessXml(root);

		this.delegate = parent;
	}

通过上面的代码我们看到了处理流程,首先是对profile的处理,然后开始进行解析,可是当我们跟进preProcessXml(root) 或者 postProcessXml(root) 时发现代码是空的,这里使用的模板方法模式,如果继承自 DefaultBeanDefinitionDocumentReader的子类需要在Bean 解析前后做一些处理的话,那么只需要从写这两个方法就可以了。

profile 属性的使用

我们注意到在注册Bean 的最开始是对PROFILE_ATTRIBUTE属性的解析,可能对于我们来说,profile属性病逝很常见。分析profile前我们先了解虾profile 的用法,官方实例代码片段如下:

<!--设置环境-->
<beans profile="dev">
    ... ...
</beans>
<beans profile="production">
    ... ...
</beans>

集成到Web 环境中时,在web.xml 中加入以下代码:

<context-param>
	<!--激活指定环境 -->
    <param-name>Spring.profiles.active</param-name>
    <param-value>dev</param-value>
</context-param>

有了这个特性我们就可以同时在配置文件中部署多套配置来适用于不同的生产环境。了解了 profile的使用再来分析代码就会清晰得多,首先程序会获取beans 节点是否定义了 profile 属性,如果定义了则需要到环境变量中去寻找。

解析并注册BeanDefinition

处理了profile 后就可以进行XML 的读取了,跟踪代码进入 parseBeanDefinitions(root, this.delegate);

DefaultBeanDefinitionDocumentReader.java
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    	// 判断当前root 是否是默认空间
		if (delegate.isDefaultNamespace(root)) {
			NodeList nl = root.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) {
				Node node = nl.item(i);
				if (node instanceof Element) {
					Element ele = (Element) node;
					if (delegate.isDefaultNamespace(ele)) {
						parseDefaultElement(ele, delegate);
					}
					else {
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
		else {
            // 使用自定义处理Element
			delegate.parseCustomElement(root);
		}
	}

在Spring 的XML 配置里面有两大类Bean声明,一个是默认的,如:

<bean id = "test" class="test.TestBean"/>

另一类就是自定义的,如:

<tx:annotation-driven/>

而两种方式的读取及解析差别是非常大的,如果采用 Spring 默认的配 Spring 当然知道该怎么做,但是如果是自定义的,那么就需要用户实现一些接口及配 对于根节点或者子节点如果是默认命名空间的话 采用 parseDefaultElement 方法进行解析,否则使用delegate.parseCustomElement 方法对自定义命名空间进行解析 而判断是否默认命名空间还是自定义命名空间的办法其实是使用 node.getNamespaceURI()获取命名空间,并与 Spring 巾固定的命名空间 http://www.springframework.org/scherna/beans 进行比对。如果一致则认为是默认,否则就认为是自定义。 而对于默认标签解析与自定义标签解析我们将会在下一 中进行讨论。

经过上面的解析根据自己的理解画出整体时序图:

在这里插入图片描述

执行步骤为:
  1. 根据路径创建ClassPathResource,将xml转为resource 资源
  2. 返回resource 资源
  3. 使用resouce资源创建 XmlBeanFactory
  4. 在XmlBeanFactory 的构造函数中调用自身另外的构造函数,参数为resource资源和 parentBeanFactory, parentBeanFactory是父BeanFactory,没有的话可以为空。
  5. 构造函数中做了两件事: 1: 使用parentBeanFacotry作为参数调用了父类的构造函数,
  6. 在父类的AbstractAutowireCapableBeanFacltory 构造函数中,调用了ignoreDependencyInterface()方法,这个方法的作用就是如果初始化的时候有依赖关系,如果实现了这些指定忽略的接口,就不会初始化。
  7. 核心方法:调用了XmlBeanDefinitionReader的loadBeanDefinitions(resource)方法来解析注册 bean
  8. 使用resource 作为参数创建 EncodeResource资源,EncodeResource 为有编码的resource资源
  9. 返回创建好的EncodeResource 资源 到XmlBeanDefinitionReader
  10. 调用自身类中的loadBeanDefinitions(encodeResource)方法,并将encodeResource 资源作为入参
  11. 获取encodeResource 中的 Resource 中的 InputStream 流
  12. 返回 inputStream 流
  13. 使用inputStream 创建 InputResource, 注意:这个inputResource 不是Spring 的,而是org.xml.sax.InputSource,sax是解析xml 的类库,他提供了很多解析xml的API
  14. 返回创建好的inputResource
  15. 调用自身类中的 doLoadBeanDefinitions(inputResource, encodeResource.getResource())方法。
  16. EntityResolver resolver = getEntityResolver(); 这个方法如果SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver 方法想SAX驱动器注册一个实例
  17. int valid= getValidationModeForResource(resource);这个方法是获取xml的验证模式,注意xml的验证模式分为两种:DTD, XSD;
  18. 调用DocumentLoadloadDocument(inputSource, resolver, this.errorHandler, vaild, isNamespaceAware());这个方法的作用是将xml转换为 Document 对象
  19. 返回 转换好的 Document 对象
  20. registerBeanDefinitions(doc, resource);核心方法这个方法的作用是解析以及注册bean
  21. registerBeanDefinitions 方法中先调用了BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader() 这个方法,这个方法的作用就是 创建BeanDefinitionDocumentReader, 因为BeanDefinitionDocumentReader是一个接口,所有实际类型是DefaultBeanDefinitionDocumentReader, 这个类的主要作用就是提供了Document 转换为BeanDefinition 的方法
  22. 调用BeanDefinitionDocumentReaderregisterBeanDefinitions(doc, createReaderContext(resource)); 这个方法的作用就是注册beanDefinition,createReaderContext(resource) 方法的作用就是创建 XmlReaderContext。
  23. 在 registerBeanDefinitions 方法里面最先操作的就是将入参XmlReaderContext 赋值给本类中的属性readerContext.
  24. 调用doRegisterBeanDefinitions(doc.getDocumentElement())重载方法;注意:doc.getDocumentElement() 方法是提取root, 并将Document 转为Element,在 doRegisterBeanDefinitions 方法中主要做了三步工作。
  25. preProcessXml(root); 这个方法默认为空,需要用户自己实现,这个方法的作用就是 解析前的处理
  26. parseBeanDefinitions(root, this.delegate); 核心方法:这个方法的作用就是解析注册Bean
  27. postProcessXml(root); 这个方法默认为空,需要用户自己实现,这个方法的作用就是 解析后的处理,

最后你会发现做了这么的工作都是在做解析注册前的准备,并没有开始真正的解析注册,真正的解析注册都在parseBeanDefinitions 方法里面,后面在将。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值