一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,​​点击查看活动详情​​。

Spring IoC容器初始化过程(xml形式)

  • IoC是如何工作的?
  • Resource定位
  • 载入BeanDefinition
  • 使用AbstractXmlApplicationContext加载resource
  • 将BeanDefiniton注册到容器-

xml解析的大致流程:

  1. 获取资源的类型
  2. 资源定位,找到资源所在的位置
  3. 利用相关的资源加载器读取资源文件载入相关的配置。
  4. 通过层层代理和委托完成最后的加载动作。

resource 定位的作用?

Resource是Spring中用于封装I/O操作的接口,在创建Spring的容器的时候,会根据xml对应配置的类型,加载不同的数据类型, 比较常见的数据类型为下面几种:

  • FileSystemResource:以文件绝对路径进行资源访问。
  • ClassPathResourcee:以类路径的方式访问资源。
  • ServletContextResource:web应用根目录的方式访问资源。
  • UrlResource:访问网络资源的实现类。 
  • ByteArrayResource: 访问字节数组资源的实现类。

Resource定位

知道了资源的类型之后,Resource是如何加载这些资源的呢?Spring提供了ResourceLoader接口用于实现不同的Resource加载策略,ApplicationContext的所有实现类都实现RecourceLoader接口,因此可以直接调用getResource(参数)获取Resoure对象。ApplicationContext也是存在子类的,根据资源的类型有不同的资源上下文,分别对应不同资源类型的加载。

但是有了资源和资源的加载器之后,资源如何注入到spring当中,spring又是如何管理的呢?这里就涉及到ioc的一个重要对象,BeanDefinition

载入BeanDefinition

BeanDefinition是根据resource对象中的bean来生成的,所以最终Bean会以BeanDefinition的形式存在,spring的配置主要为xml的格式,所以会使用AbstractXmlApplicationContext进行加载,其中有一个loadBeanDefinitions(DefaultListableBeanFactory beanFactory) 方法获取。

在代码内,存在一个叫做BeanDefinitionReader的对象用于解析bean的xml属性,接着BeanDefinitionReader会设置相关的环境和注入当前的上下文对象,注入一个属性解析用于解析xml属性内容。最后通过当前的上下文载入beandefine。

Spring IoC容器初始化过程(xml形式)_xml

我们顺着this.loadBeanDefinition(), 在内部通过xmlBeanDefinitionReader进行操作,分别加载resourse和本地的配置位置。加载的细节在xmlBeanDefinitionReader当中,我们接着顺着xmlBeanDefinitionReader.loadBeanDefinitions看下他是如何加载资源的。

Spring IoC容器初始化过程(xml形式)_xml_02

可以看到,加载资源是重载了一个AbstractBeanDefinitionReader的loadBeanDefinitions方法,其实可以看作是讲加载资源的操作最终委托给了xmlBeanDefinitionReader

Spring IoC容器初始化过程(xml形式)_后端_03

下面是相关的内容和方法:

Spring IoC容器初始化过程(xml形式)_xml_04 从上面的代码可以看到实际调用的就是子类加载资源的方法,由于这里是XML的方式加载我们可以接着看一下:

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);
}
// 从threadlocal上下文中获取
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 讲资源绑定到当前线程
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
// 设置编码,同事加载IO流
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
// 加载完成之后需要进行卸载操作
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

// 加载资源
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;
}
// 根不同异常进行处理
}

接着是xml加载资源的具体细节,首先在加载资源之前,需要对于资源进行一次校验的操作,然后从资源当中加载document对象,大致过程为:从resource中吧资源文件读到document中,那么如何读取到document中?这里借助了documentLoader对象进行加载,XMLBeanDefine会创建一个DefaultDocumentLoader的私有属性,使用loadDocument方法加载,下面根据描述跟进一下具体代码:

/**
getValidationModeForResource(resource) 校验过后的资源
isNamespaceAware() 命名空间感知
getEntityResolver()
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}

内部的细节不做过多的拓展,接着来看一下registerBeanDefinitions(doc, resource),这个方法的主要内容是对于Spring的内容进行语义的转化,变为beanDefinision的类型,这里同样通过构建一个BeanDefinitionDocumentReader对象完成创建的工作。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 获取BeanDefinitionDocumentReader实例
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 获取容器当中bean的数量
int countBefore = getRegistry().getBeanDefinitionCount();
// 注入bean并且设置资源上下文
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

接着我们可以看一下注入bean的具体操作,这里关注registerBeanDefinition方法,

protected void doRegisterBeanDefinitions(Element root) {
/*任何嵌套的 <beans> 元素都将导致此方法中的递归。 在
为了正确传播和保留 <beans> default-* 属性,
跟踪当前(父)委托,它可能为空。 创建
新的(子)委托,带有对父级的引用,用于回退目的,
然后最终将 this.delegate 重置回其原始(父)引用。
这种行为模拟了一堆委托,而实际上并不需要一个。
*/
BeanDefinitionParserDelegate parent = this.delegate;
// 创建委托对象BeanDefinitionParserDelegate,将dom解析委托给BeanDefinitionParserDelegate完成
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// 我们不能使用 Profiles.of(...) 因为 XML 配置中不支持配置文件表达式。 有关详细信息,请参阅 SPR-12458。
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;
}

这里可能会有疑问,都已经进行dom解析了为什么还需要一个委托对象来完成?BeanDefinitionParserDelegate 对象这里涉及到JDK和CGLIB动态代理的知识,简单来说这个代理类会完成符合SPring bean语义规则的处理,比如BeanDefinitionParseDelegate代理中的parseBeanDefinition会对于xml文件中的节点解析,通过遍历import标签节点调用importBeanDefinitionResource方法处理,接着利用processBeanDefinition方法进行处理。

记者我们看一下核心方法的内容,这个比较好理解,就是一个典型的dom解析工作:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
// 遍历节点,进行解析的操作,遍历到《import》标签节点调用importBeanDefinitionResource(ele)进行处理
// 遍历到bean 使用
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);
}
}

// 对于dom内容进行解析,
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

这里选择标签的解析作为结尾,可以看到这里传递了两个参数,Element节点和一个delegate对象,首先通过delegate代理对象完成BeanDefineition的容器注册动作,然后通过工具类BeanDefinitionReaderUtils 注册最终的bean,最终完成bean的加载操作。

写在最后

当然这个过程只是简单回顾一下这种加载方式在现在其实也早就被抛弃了。现在更为习惯使用Java类的加载方式,其实形式大同小异,只是细节有差异而已。