【死磕 Spring】—— IoC 之加载 BeanDefinition

前言

​ 先来一段熟悉的代码:

ClassPathResource resource = new ClassPathResource("bean.xml"); // <1>
DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); // <2>
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); //<3>
reader.loadBeanDefinitions(resource); // <4>

​ 这段代码是Spring 中编程式使用Ioc 容器,通过这四段简单的代码,我们可以初步判断 Ioc 容器的使用过程。

  1. 获取资源

  2. 获取BeanFactory

  3. 根据新建的BeanFactory 创建一个BeanDefinitionReader 对象,该Reader 对象为资源的解析器

  4. 装载资源

    整个过程就分为三个步骤:资源定位、装载、注册,如下:

image-20220406173130233
  • 资源定位:我们一般使用外部资源来描述Bean 对象,所以在初始化 Ioc 容器的第一步就是需要定位这个外部资源。在上一篇博客,已经详细说明了资源加载的过程。

  • 装载:装载就是BeanDefinition 的载入。BeanDefinition 读取、解析Resource资源,也就是将用户定义的Bean 表示成Ioc 容器的内部数据结构。

    • 在Ioc 容器内部维护着一个 BeanDefinition Map 的数据结构
    • 在配置文件中, 每一个 都对应着一个 BeanDefinition 对象

    本文,我们分享的就是装载这个步骤。

  • 注册: 向Ioc 容器注册在第二部解析好的BeanDefinition,这个过程是通过BeanDefinitionRegistry 接口来实现的。在Ioc 容器内部其实是将第二个过程解析得到的 BeanDefinition 注入到一个HashMap 容器中,Ioc 容器就是通过这个HashMap 来维护这些 BeanDefinition的。

    • 在这里需要注意一点是这个过程并没有完成依赖注入 Bean创建,Bean创建是发生在应用第一次调用 #getBean(...) 方法,想容器索要 Bean 时
    • 当然我们可以通过设置预处理,即对某个 Bean 设置,lazyinit = false 属性,那么这个Bean 的依赖注入就会在容器初始化的时候完成。

简单的说,上面步骤的结果就是: XML Resource => XML Document => Bean Definition

1. loadBeanDefinitions

​ 资源定位在前面已经分析过了,下面,我们直接分析加载,上面看到的 reader.loadBeanDefinitionsa(resource) 代码,才是加载资源真正的实现,所以我们直接从该方法入手。代码如下:

// XmlBeanDefinitionReader.java
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
	return loadBeanDefinitions(new EncodedResource(resource));
}
  • 从执行的 xml 文件加载 Bean Definition, 这里会吸纳对 Resource 资源封装成 org.springframework.core.io.support.EncodeResource对象。这里为什么需要将 Resource封装成 EncodeResource 呢?主要是为了对Resource 进行编码,保证内容的可读性,
  • 然后,在调用 #loadDefinitions(EncodedResource encodeResource) 方法,执行真正的逻辑实现。
/**
 * 当前线程,正在加载的 EncodedResource 集合。
 */
private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded = new NamedThreadLocal<>("XML bean definition resources currently being loaded");

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);
	}

	// <1> 获取已经加载过的资源
	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 {
		// <2> 从 EncodedResource 获取封装的 Resource ,并从 Resource 中获取其中的 InputStream
		InputStream inputStream = encodedResource.getResource().getInputStream();
		try {
			InputSource inputSource = new InputSource(inputStream);
			if (encodedResource.getEncoding() != null) { // 设置编码
				inputSource.setEncoding(encodedResource.getEncoding());
			}
			// 核心逻辑部分,执行加载 BeanDefinition
			return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
		} finally {
			inputStream.close();
		}
	} catch (IOException ex) {
		throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), ex);
	} finally {
		// 从缓存中剔除该资源 <3>
		currentResources.remove(encodedResource);
		if (currentResources.isEmpty()) {
			this.resourcesCurrentlyBeingLoaded.remove();
		}
	}
}
  • <1> 处,通过resourcesCurrentlyBeingLoaded.get() 代码,来获取已经加载过的资源,然后将encodeResource 加入其中,如果,resourcesCurrentlyBeingLoaded 中已经存在该资源,则抛出BeanDefinitionStoreException异常。
    • 为什么需要这么做呢? 答案在 "detected cyclic loading", 避免一个EncodedResource 在加载时,还没有加载完成,又加载自身,从而导致死循环
    • 也因此,在<3> 处,当一个EncodedResource 加载完成后,需要从缓存中提出
  • <2> 处理,从encodedResource 获取封装后的Resource资源,并从Resource 中获取相应的InputStream, 然后将 InputStream封装成InputSource,最后调用#doLoadBeanDefinitions(InputSource inputSource, Resource resource) 方法,执行加载BeanDefinition 的真正逻辑。

2. doBeanDefinitions

/**
 * Actually load bean definitions from the specified XML file.
 * @param inputSource the SAX InputSource to read from
 * @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
 * @see #doLoadDocument
 * @see #registerBeanDefinitions
 */
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
		throws BeanDefinitionStoreException {
	try {
		// <1> 获取 XML Document 实例
		Document doc = doLoadDocument(inputSource, resource);
		// <2> 根据 Document 实例,注册 Bean 信息
		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);
	}
}
  • <1>处,调用 #doLoadDocument(ImputSource inputSource, Resource resource 方法,根据xml 文件,获取Document 对象实例。
  • <2>处,调用 #registerBeanDefinitions(Document doc, Resource resource) 方法,根据获取的 Document 实例,注册 Bean 信息。

2.1 doLoadDocument

/**
 * 获取 XML Document 实例
 *
 * Actually load the specified document using the configured DocumentLoader.
 * @param inputSource the SAX InputSource to read from
 * @param resource the resource descriptor for the XML file
 * @return the DOM Document
 * @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());
}
posted @ 2022-04-07 13:57  雷姆饲养员  阅读(55)  评论(0)    收藏  举报