Spring 读取 XML 的机制
综述
本文主要记录 Spring 读取 XML 并完成 BeanDefinition
注册的一些基本步骤。
介绍思路
我们从一个简单的例子开始走,逐步完成对整个流程的记录。
源码解析
总览
public class MyTest {
public static void main(String[] args){
XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource(""));
beanFactory.getBean("");
}
}
以上是一个基本的基于 XML 的 Spring 框架使用配置方法。我们从 XmlBeanFactory
开始入手,首先需要了解XmlBeanFactory
的继承关系:
其中,各接口定义的主要功能罗列如下:
对别名注册相关操作
AliasRegistry
: 定义对 alias 的简单增删改查BeanDefinitionRegistry
:定义 bd 注册相关操作
对单例相关操作
SingletonBeanRegistry
: 定义对单例 Bean 的注册、获取
Bean 工厂相关操作:
BeanFactory
: 定义获取 Bean 及 Bean 的各种属性ListableBeanFactory
: 定义中增加按条件获得相关 bean 配置的接口HierarchicalBeanFactory
: 定义中增加对BeanFactory
分层的支持【parentFactory
】AutowireCapableBeanFactory
: 定义中增加对Factory
中Bean
的创建、自动注入、初始化、调用注册在Factory
中的后处理器钩子的支持
综合:
ConfigurableBeanFactory
:结合分层Bean工厂和单例注册的功能。提供支持单例、原型生命周期的 Bean 工厂,并提供一些其他的Factory
配置属性ConfigurableListableBeanFactory
: 定义中支持多种生命周期,支持Bean创建、自动注入、初始化、后处理器钩子,按条件筛选Factory
中注册的bean
上面仅介绍了各个接口的功能定义,总体来说思路还是比较清晰的:
分别定义了 Factory 工厂、别名相关、单例注册相关接口,然后各自完善信息,最后由一两个接口开始陆续将功能集合起来。
我们接下来看 class 类的功能扩展,class 通过继承来得到父类的逻辑,同时通过实现接口,增加对接口功能的支持,得益于Java的单继承,这条线还是非常清晰的。
XmlBeanFactory
内部原理
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
XmlBeanFactory
在内部创建了一个 XmlBeanDefinitionReader
类实例,并将自身传给它,在调用构造函数时,会委托 reader
读取xml文件并将BeanDefinition
保存至XmlBeanFactory
中,等待后面调用getBean()
进行实例化。
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
解析 XML 之前的准备工作
XmlBeanFactory
将读取 XML 并注册的工作交给了 XmlBeanDefinitionReader
的loadBeanDefinitions()
,我们接下来看看他的逻辑。
其实XmlBeanDefinitionReader
本身不进行 xml 文件的读取,它将 xml 文件的解析委托给了 JDK 的xml读取框架,它的主要作用是在拿到了解析之后的Document
节点树之后进行解析并生成对应的 BeanDefinition
。
loadBeanDefinitions(Resource)
/**
* 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));
}
Spring 考虑到逻辑的模块划分,往往不会在一个函数中做过多的逻辑操作,每个函数都只做属于自己的一部分逻辑,如此使程序的可读性大大提升,且后续增加条件限制、进行代码复用会更加方便。
此处主要对 Resource
进行了一层包转,以避免在读配置内容时因编码问题引起的乱码。
loadBeanDefinitions(EncodedResource)
/**
* Load bean definitions from the specified XML file.
* 此方法向外暴露,仅进行一些前置校验及处理工作,具体逻辑会委托给 doXXXX 函数
*
* @param encodedResource the resource descriptor for the XML file,
* allowing to specify an encoding to use for parsing the file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
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);
}
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// currentResources 是线程安全的 Set 【不考虑反射啥的极端情况】,添加失败的原因是该对象已经存在,所以无法添加
// 所以,如果添加失败,说明这个正在被此线程解析时被第二次解析了,有循环引用
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// 获得输入流
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// 用 SAX 框架的 InputSource 包一下,方便后面直接调用该框架进行 xml 格式文件的解析
// 详见 https://www.cnblogs.com/nerxious/archive/2013/05/03/3056588.html
// 思路还是很简单的,就是把 xml 的那些 "闭合标签 + 属性" 翻译成了一棵用数据结构表示的树
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 将具体实现委托给 doXXXXX
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();
}
}
}
此函数的思路很简单,和 Spring 在创建 Bean 实例的思路一样:
- 先在创建前记录要开始读取资源文件
- 从资源文件中读到输入流
- 将输入流委托给
doLoadBeanDefinitions()
进行解析、注册 - 完成解析,删除之前对开始读文件的记录
在整个解析过程中都用resourcesCurrentlyBeingLoaded
进行了记录,也因此避免了循环加载。【注意,这里采用的 ThreadLocal
我认为可能是因为在某些框架中,可能有多个上下文环境,各自注册自己的 Bean ,不同上下文可以重复个自创建个自的 Bean ,不会互相冲突;同时,同一个上下文是不允许循环依赖的,这样加载起来就没完没了了】
doLoadBeanDefinitions(InputSource,Resource)
/**
* 从指定的 xml 中加载 BeanDefinition
*
* @param inputSource 用来读取 xml 信息的输入流
* @param resource 上面输入流的描述,我们在生成 BeanDefinition 时要把 Resource 一起放进去,这样
* 在后面 Bean 生成实例出问题时方便排查,可能有些 BeanDefinition 还需要从 Resource 获得
* 一些属性
* @return 新创建的 BeanDefinition 数量
* @throws BeanDefinitionStoreException in case of loading or parsing errors
* @see #doLoadDocument
* @see #registerBeanDefinitions
*/
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// 将 xml 中的元素解析成 Document 对象中的数据
Document doc = doLoadDocument(inputSource, resource);
// 注册 bean
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 框架,完成 XML 的解析
- 根据解析之后的树,完成 BeanDefinition 的生成、注册【当然,此处默认命名空间的 Spring 做了,也留出足够的配置空间给那些基于 Spring 的框架来自行定义命名控价、解析规则】
读取 XML 并转化成 DOM 树
上面我们发现最后做的只有两件事:读 XML 和生成 BeanDefinition。我们先看一下是怎么读取 XML 生成 DOM 树的。
Document doc = doLoadDocument(inputSource, resource);
此处没有像 Spring 往常那样继续玩一下 loadDocument()
和doLoadDocument()
的函数分层,直接委托给了doLoadDocument()
。
doLoadDocument()
/**
* 用指定的 documentLoader 读取 xml 文件
*
* @param inputSource SAX 框架用来读 xml 的输入流
* @param resource xml 文件的信息
* @return DOM 树
* @throws Exception when thrown from the DocumentLoader
* @see #setDocumentLoader
* @see DocumentLoader#loadDocument
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,getValidationModeForResource(resource), isNamespaceAware());
}
这里直接将读取操作委托给了 DocumentLoader
,给我的感觉有点像适配器的样子,将复杂入参的接口中确定的入参配置好,仅将需要变化的入参暴露出去。
loadDocument()
这个是 DocumentLoader
接口中定义的方法,由于 SAX 框架的调用是模版样式的,所以我们直接粘进来:
/**
* 从 inputSource 中加载出一个 Document
*
* @param inputSource 要解析成 DOM 树的输入流
* @param entityResolver 这个是一些用来解析内部实体的解析器,为了那些基于 Spring 框架的其他框架定
* 制自己的命名空间后需要有指定的解析器才能实现对应的逻辑
* @param errorHandler 一个报警处理器,专为 SAX 设置
* @param validationMode XML 的定义是 DTD 还是 XSD 【其实根据情况 XSD 居多】
* @param namespaceAware SAX的一个参数,设置成 true ,解析出的 Document 会带着命名空间
* @return the loaded {@link Document document}
* @throws Exception if an error occurs
*/
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(namespaceAware);
if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
factory.setValidating(true);
if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
// Enforce namespace aware for XSD...
factory.setNamespaceAware(true);
try {
factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
} catch (IllegalArgumentException ex) {
ParserConfigurationException pcex = new ParserConfigurationException(
"Unable to validate using XSD: Your JAXP provider [" + factory +
"] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
"Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
pcex.initCause(ex);
throw pcex;
}
}
}
return factory;
}
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
@Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
throws ParserConfigurationException {
DocumentBuilder docBuilder = factory.newDocumentBuilder();
if (entityResolver != null) {
docBuilder.setEntityResolver(entityResolver);
}
if (errorHandler != null) {
docBuilder.setErrorHandler(errorHandler);
}
return docBuilder;
}
都是一些模版代码,后面自己写的demo基本就明白了。
根据读取的 DOM 树进行 BeanDefinition 的生成、注册
上一节在介绍 Document doc = doLoadDocument(inputSource, resource);
的内部逻辑,接下来了解后面的registerBeanDefinitions(doc, resource)
,本节介绍 BeanDefinition
的生成。
/**
* 生成 BeanDefinition ,并注册。
* 返回注册前后的差值,也就是此次注册的 BeanDefinition 个数
*
* @param doc the DOM document
* @param resource the resource descriptor (for context information)
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of parsing errors
* @see #loadBeanDefinitions
* @see #setDocumentReaderClass
* @see BeanDefinitionDocumentReader#registerBeanDefinitions
*/
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
createBeanDefinitionDocumentReader()
protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
return BeanUtils.instantiateClass(this.documentReaderClass);
}
创建一个BeanDefinitionDocumentReader
实例,将 Document 解析成对应的 BD 。至于每次都重新创建的原因,猜测是因为这个类内部有状态。
createReaderContext()
public XmlReaderContext createReaderContext(Resource resource) {
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, getNamespaceHandlerResolver());
}
创建 BeanDefinition 创建的上下文,主要用于将此次创建所需的资源、XmlBeanDefinitionReader
的相关监听器传过去。由于 resource
每次都不一样,所以每次都创建新的XmlReaderContext
实例是可以理解的。
注意一个点,在创建实例时将 this
穿进去了,而在本实例中由有对XmlBeanFactory
的引用,也因此可以实现将创建的 BD 注册到对应的Factory
中。
documentReader.registerBeanDefinitions()
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
思路很清晰,设置上下文,然后从根节点开始调用 doRegisterBeanDefinitions()
。其实,根据我们的理解,每次新的上下文【resource
不同】都会新创建 documentReader
实例,我们直接在createBeanDefinitionDocumentReader()
时也就是初始化时将上下文放进去,不是更加简洁么。。。。
doRegisterBeanDefinitions()
具体逻辑
/**
* 对 root 下的所有 符合条件的 DOM 节点进行处理,生成 BD 并注册
*/
protected void doRegisterBeanDefinitions(Element root) {
// 根节点应该是 <beans />标签,根节点下也可以有 <beans />标签,标签的嵌套也意味着此函数的递归调用。
//
// 为了保证递归之后在进行调用时仍能获得标签父节点的配置,我们使用 this.delegate 进行记录,先在操作前
// 保存当前值,然后该怎么用怎么用,最后再归位。在单线程的情况下完成了整个切换,原理是借助函数调用栈。
//
// 保存之前的
BeanDefinitionParserDelegate parent = this.delegate;
// 生成自己的
this.delegate = createDelegate(getReaderContext(), root, parent);
// 如果是默认的命名空间,就配置一下
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); //得到 xml 配置的 profile
if (StringUtils.hasText(profileSpec)) { // 如果有值就需要判断一下
//根据分隔符分开
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// profile 环境满足,就继续生成 BD并注册,否则直接跳过整个 <beans /> 标签
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("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;
}
主要是对 <beans />
标签的 profile
进行一下条件判断,看是否需要注册、解析,并调用钩子对 xml 生成的节点进行一些处理工作。
parseBeanDefinitions(root, this.delegate)
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
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 {
delegate.parseCustomElement(root);
}
}
对传入的节点及子节点 根据命名空间选择合适的解析方法。如果是默认命名空间就使用parseDefaultElement()
,否则使用parseCustomElement()
。
到此,我们完成了 Spring 的 从 XML 配置的外层解析,后买呢我们会对默认命名空间的解析和自定义命名控价的解析专门介绍。
扩展:一些在读源码时遇到的数据结构
Resource
Resource
是 Spring 创建的一个资源定位接口,Spring 没有直接使用 Java 中的 URL 框架,因为该框架实现机制比较复杂,提供的方法也不够全面。大概看一下,熟悉 Resource
接口中的几个方法即可,不必看实现。
具体参见专门介绍这个的文章。
BeanDefinition
存储 bean 的定义,有问题参考专门介绍的文章。
DocumentLoader
此接口只有一个实现类,里面使用 SAX 框架进行了 xml 文件的解析。
扩展: SAX 框架了解
这个博客记录了一些 Java 读入 XML 文件的东西。就是 API 而已。不再赘述。
https://blog.csdn.net/jxufecodelong/article/details/16591369