Spring源码阅读笔记03:xml配置读取
前面的文章介绍了IOC的概念,Spring提供的bean容器即是对这一思想的具体实现,在接下来的几篇文章会侧重于探究这一bean容器是如何实现的。在此之前,先用一段话概括一下bean容器的基本工作原理。顾名思义,bean容器的作用是替我们管理bean对象(简单的Java类对象)的。不管框架如何强大,还是需要我们程序员来告诉其一些必要信息的(比如要管理的bean对象的类相关信息、是否开启组件扫描等),这些我们称之为对Spring框架的配置,目前主流的配置方式是通过使用配置文件或注解。配置好之后,框架就需要将这些配置读取并保存到内存中(其实就是保存在对象里面)。经过这一步转化之后,Spring框架就能够帮助我们加载指定的类,然后将其实例化并且缓存起来以供需要的时候直接使用,这就是容器。当我们将容器关闭时,Spring框架会将之前创建的所有相关对象全部销毁,并释放资源。
如上只是简单介绍了一下Spring提供的bean容器的基本工作原理,从中能够了解大体流程即可,真实的容器其工作原理远远比这复杂。本文主要总结Spring对配置的读取以及将配置转化保存到内存这部分,并且配置获取这部分的源码也只限于对xml配置文件的读取。
上面说到的配置读取及初始化的功能对应前面文章中的代码看起来只有区区一行,如下:
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
这行代码做了两件事情:
- 将xml配置文件封装成Resource;
- 初始化BeanFactory;
1. 配置文件封装
Spring的配置文件读取功能是封装在ClassPathResource中,对应前面的代码就是new ClassPathResource("bean.xml"),那么ClassPathResource又是做什么的呢?这个需要从头说起。
其实呢Spring将其内部使用到的资源的获取方式独立抽取出来,通过Resource接口来封装底层资源,其接口定义如下:
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
} public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
InputStreamSource是一个接口,它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象,该接口定义任何能返回InputStream的类,比如file普通文件、Classpath下的资源文件和ByteArray等。
Resource接口用于抽象所有Spring内部使用到的底层资源:File、URL、Classpath等,其定义了一系列方法:
- 首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen);
- 另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法;
- 为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative();
- 在错误处理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用于在错误处理中打印信息;
Spring中对不同来源的资源文件类型都有相应的Resource实现:文件(FileSystemResource)、Classpath资源( ClassPathResource)、URL资源(UrIResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。
Resource接口的作用是消除底层资源访问的差异,允许程序以一致的方式来访问不同的底层资源,而其实现是非常简单的,以getInputStream()方法实现为例,ClassPathResource中的实现方式是直接调用class或者classLoader提供的底层方法getResourceAsStream,而对于FileSystemResource的实现其实更简单,直接使用FileInputStream对文件进行实例化。
// ClasspathResource.java
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAs你Stream(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
public InputStream getInputStream()throws IOException{
return new FileInputStream(this.file);
}
这样就可以将资源统一转化成InputStream供后续使用了,而前面示例代码中使用的xml配置文件是属于什么类型的Resource呢?其实从源码中我们就不难发现是属于ClassPathResource的,而new ClassPathResource("bean.xml")这句代码的内部实现就不细说了,无非就是初始化配置文件路径。
一句话总结,Spring通过Resource接口抽象所有的资源,在容器启动的第一步就是将资源文件映射成Resource对象,以供后续通过流的方式来获取资源。
现在配置文件封装到Resource中之后,后续Spring在初始化BeanFactory的过程中就可以方便地调用其getInputStream()方法来获取其对应的流了,然后做进一步转化。
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, null);
} public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
这段代码主要作用是初始化BeanFactory,其中this.reader.loadBeanDefinitions(resource)就是资源加载的真正实现,也是我们接下来的分析重点。
2. 转换成beanDefinition
我们来看一下loadBeanDefinitions()方法具体的内部实现:
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
} public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
// 通过属性来记录已经加载的资源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// 从encodeResource中获取封装的Resource对象并再次从Resouce中获取其中的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();
}
}
}
这一部分其实还只是数据准备阶段,主要做了如下三件事情:
- 封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装;
- 获取输入流。从Resource中获取对应的InputStream并构造InputSource;
- 通过构造好的InputSource实例和Resource实例继续调用方法:doLoadBeanDefinitions,这是真正的核心处理部分;
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
int validationMode = getValidationModeForResource(resource);
Document doc = this.documentLoader.loadDocument(
inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
return registerBeanDefinitions(doc, resource);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
。。。catch若干异常
}
不考虑其中异常类的代码,这段代码其实只做了三件事:
- 获取XML文件的验证模式;
- 加载XML文件,并得到对应的Document;
- 根据返回的Document注册Bean信息;
这3个步骤支撑着整个Spring容器部分的实现基础,尤其是第3步对配置文件的解析,逻辑非常的复杂,这里我们只分析第2步和第3步。
2.1 获取Document
XmIBeanFactoryReader类对于文档读取并没有亲力亲为,而是委托给了DocumentLoader去执行,这里的DocumentLoader只是个接口,实际对象则是DefaultDocumentLoader,解析代码如下:
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);
}
首先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,然后解析inputSource来返回Document对象。这部分是JDK提供的功能,有兴趣的可以自行搜索,此处就不再赘述。
2.2 解析及注册BeanDefinitions
当把文件转换为Document后,接下来的提取及注册bean就是重头戏。继续上面的分析,当程序已经拥有XML文档文件的Document实例对象时,就会被引入下面这个方法:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 将环境变量设置其中
documentReader.setEnvironment(getEnvironment());
// 在实例化BeanDefinitionReader时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory的子类
// 记录统计前BeanDefinition的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 加载及注册bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 记录本次加载的BeanDefinition个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
在这个方法中,加载及注册bean的逻辑是委托给BeanDefinitionDocumentReader指向的类来处理,这很好地应用了面向对象中单一职责的原则。BeanDefinitionDocumentReader是一个接口,其实例化的工作是在 createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader的registerBeanDefinitions()方法后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册:
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
终于到了核心逻辑的底部doRegisterBeanDefinitions(root),如果说之前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions算是真正地开始进行解析了。
protected void doRegisterBeanDefinitions(Element root) {
// 处理profile属性
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
// 专门处理解析
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(this.readerContext, root, parent);
// 解析前处理,留给子类实现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
// 解析后处理,留给子类实现
postProcessXml(root); this.delegate = parent;
}
这里首先是对profile的处理,然后开始进行解析,preProcessXml(root)和postProcessXml(root)方法是空实现,留待用户继承DefaultBeanDefinitionDocumentReader后需要在Bean解析前后做一些处理时重写这两个方法。跟踪代码进入parseBeanDefinitions(root, this.delegate):
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 对beans的处理
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)) {
// 对bean的处理
parseDefaultElement(ele, delegate);
}
else {
// 对bean的处理
delegate.parseCustomElement(ele);
}
}
}
}
else {
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/schema/beans进行比对。如果一致则认为是默认,否则就认为是自定义。
3. 总结
本文主要总结Spring对配置(xml配置文件)的读取以及将配置转化保存到内存这部分,对xml配置的读取主要是将其转换成Resource,而将配置转化保存则主要是从Resource中获取InputStream并将其解析转化成BeanDefinition等对象保存起来。
Spring源码阅读笔记03:xml配置读取的更多相关文章
- Spring源码阅读笔记02:IOC基本概念
上篇文章中我们介绍了准备Spring源码阅读环境的两种姿势,接下来,我们就要开始探寻这个著名框架背后的原理.Spring提供的最基本最底层的功能是bean容器,这其实是对IoC思想的应用,在学习Spr ...
- Spring源码阅读笔记
前言 作为一个Java开发者,工作了几年后,越发觉力有点不从心了,技术的世界实在是太过于辽阔了,接触的东西越多,越感到前所未有的恐慌. 每天捣鼓这个捣鼓那个,结果回过头来,才发现这个也不通,那个也不精 ...
- Spring源码阅读笔记01:源码阅读环境准备
1. 写在前面 对于做Java开发的同学来说,Spring就像是一条绕不过去的路,但是大多数也只是停留在对Spring的简单使用层面上,对于其背后的原理所知不多也不愿深究,关于这个问题,我在平时的生活 ...
- Spring源码阅读笔记05:自定义xml标签解析
在上篇文章中,提到了在Spring中存在默认标签与自定义标签两种,并且详细分析了默认标签的解析,本文就来分析自定义标签的解析,像Spring中的AOP就是通过自定义标签来进行配置的,这里也是为后面学习 ...
- spring源码阅读笔记06:bean加载之准备创建bean
上文中我们学习了bean加载的整个过程,我们知道从spring容器中获取单例bean时会先从缓存尝试获取,如果缓存中不存在已经加载的单例bean就需要从头开始bean的创建,而bean的创建过程是非常 ...
- spring源码阅读笔记09:循环依赖
前面的文章一直在研究Spring创建Bean的整个过程,创建一个bean是一个非常复杂的过程,而其中最难以理解的就是对循环依赖的处理,本文就来研究一下spring是如何处理循环依赖的. 1. 什么是循 ...
- Spring源码阅读笔记04:默认xml标签解析
上文我们主要学习了Spring是如何获取xml配置文件并且将其转换成Document,我们知道xml文件是由各种标签组成,Spring需要将其解析成对应的配置信息.之前提到过Spring中的标签包括默 ...
- spring源码阅读笔记08:bean加载之创建bean
上文从整体视角分析了bean创建的流程,分析了Spring在bean创建之前所做的一些准备工作,并且简单分析了一下bean创建的过程,接下来就要详细分析bean创建的各个流程了,这是一个比较复杂的过程 ...
- spring源码阅读笔记10:bean生命周期
前面的文章主要集中在分析Spring IOC容器部分的原理,这部分的核心逻辑是和bean创建及管理相关,对于单例bean的管理,从创建好到缓存起来再到销毁,其是有一个完整的生命周期,并且Spring也 ...
随机推荐
- c语言中fflush的运用为什么没有效果呢,测试平台linux
/************************************************************************* > File Name: clearing. ...
- 【lca+输入】Attack on Alpha-Zet
Attack on Alpha-Zet 题目描述 Space pirate Captain Krys has recently acquired a map of the artificial and ...
- vue项目中的elementUI的table组件导出成excel表
1.安装依赖:npm install --save xlsx file-saver 2.在放置需要导出功能的组件中引入 import FileSaver from 'file-saver' impor ...
- usb转串口驱动安装失败
方法一:通过驱动精灵安装 方法二:手动下载后安装,下载地址如下http://www.wch.cn/download/CH341SER_EXE.html https://blog.csdn.net/ja ...
- USB Reverse Tether (a dirty solution)
Tether your android phone to your PC using USB cable could share your 3g Internet connection with PC ...
- CentOS下MySQL忘记root密码解决方法【亲测】
1.修改MySQL的登录设置: # vim /etc/my.cnf 在[mysqld]的段中加上一句:skip-grant-tables 例如: [mysqld] datadir=/var/lib/m ...
- SpringMVC配置讲解(一)
SpringMVC 核心类和接口 DispatcherServlet -- 前置控制器 在DispatcherServlet的初始化过程中,框架会在web应用的 WEB-INF文件夹下寻找名为[s ...
- kaggle——绝地求生游戏最终排名预测
绝地求生游戏最终排名预测 知识点 数据读取与预览 数据可视化 构建随机森林预测模型 导入数据并预览 先导入数据并预览.本次实验同样来源于 Kaggle 上的一个竞赛: 绝地求生排名预测 ,由于原始数据 ...
- [LC] 151. Reverse Words in a String
Given an input string, reverse the string word by word. Example 1: Input: "the sky is blue" ...
- 1)PHP,数据库操作类网址
(1)脚本之家 http://www.jb51.net/article/94347.htm (2)一个博客 http://www.cnblogs.com/lvchenfeng/p/5003629.ht ...