跳至主要內容

4.BeanDefinition 的加载阶段(XML 文件)

Java突击队大约 10 分钟

4.BeanDefinition 的加载阶段(XML 文件)

该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址open in new window 进行阅读

Spring 版本:5.1.14.RELEASE

开始阅读这一系列文章之前,建议先查看《深入了解 Spring IoC(面试题)》open in new window这一篇文章

该系列其他文章请查看:《死磕 Spring 之 IoC 篇 - 文章导读》open in new window

BeanDefinition 的加载阶段(XML 文件)

上一篇文章 《Bean 的“前身”》open in new window 对 BeanDefinition 进行了介绍,Bean 是根据 BeanDefinition 配置元信息对象生成的。我们在 Spring 中通常以这两种方式定义一个 Bean:面向资源(XML、Properties)面向注解,那么 Spring 是如何将这两种方式定义的信息转换成 BeanDefinition 对象的,接下来会先分析**面向资源(XML、Properties)**这种方式 Spring 是如何处理的

下来熟悉一段代码:

dependency-lookup-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
	<!-- <context:component-scan base-package="org.geekbang.thinking.in.spring.ioc.overview" /> -->

    <bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User">
        <property name="id" value="1"/>
        <property name="name" value="小马哥"/>
    </bean>
</beans>

// 创建 BeanFactory 容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// XML 配置文件 ClassPath 路径
String location = "classpath:/META-INF/dependency-lookup-context.xml";
// 加载配置
int beanDefinitionsCount = reader.loadBeanDefinitions(location);
System.out.println("Bean 定义加载的数量:" + beanDefinitionsCount);
// 依赖查找
System.out.println(beanFactory.getBean("user"));;

这段代码是 Spring 中编程式使用 IoC 容器,我们可以看到 IoC 容器的使用过程大致如下:

1、 创建BeanFactory对象(底层IoC容器);
2、 创建BeanDefinitionReader对象(资源解析器),关联第1步创建的BeanFactory;
3、 通过BeanDefinitionReader加载XML配置文件资源,解析出所有的BeanDefinition对象;
4、 进行依赖查找;

上面的第 3 步会解析 Resource 资源,将 XML 文件中定义的 Bean 解析成 BeanDefinition 配置元信息对象,并往 BeanDefinitionRegistry 注册中心注册,此时并没有生成对应的 Bean 对象,需要通过依赖查找获取到 Bean。当然,我们在实际场景中一般不会这样使用 Spring,这些工作都会有 Spring 来完成。接下来我们一起来看看 Sping 是如何加载 XML 文件的

BeanDefinitionReader 体系结构

org.springframework.beans.factory.support.BeanDefinitionReader 接口的类图如下所示:

 

总览:

  • org.springframework.beans.factory.support.BeanDefinitionReader 接口,BeanDefinition 读取器
  • org.springframework.beans.factory.support.AbstractBeanDefinitionReader 抽象类,提供通用的实现,具体的资源加载逻辑在由子类实现
  • org.springframework.beans.factory.xml.XmlBeanDefinitionReader,XML 文件资源解析器,解析出 BeanDefinition 配置元信息对象并注册
  • org.springframework.beans.factory.support.PropertiesBeanDefinitionReader,Properties 文件资源解析器

BeanDefinitionReader 接口

org.springframework.beans.factory.support.BeanDefinitionReader 接口,BeanDefinition 读取器,定义了加载资源的方法,代码如下:

public interface BeanDefinitionReader {

	/** 返回 BeanDefinition 注册中心 */
	BeanDefinitionRegistry getRegistry();

	/** 返回 Resource 资源加载器,默认为 PathMatchingResourcePatternResolver */
	@Nullable
	ResourceLoader getResourceLoader();

	/** 返回类加载器 */
	@Nullable
	ClassLoader getBeanClassLoader();

	/** 返回 Bean 的名称生成器,默认为 DefaultBeanNameGenerator */
	BeanNameGenerator getBeanNameGenerator();
	/** 从 Resource 资源中加载 BeanDefinition 并返回数量 */
	int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;

	int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
    
	int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;

	int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}

AbstractBeanDefinitionReader 抽象类

org.springframework.beans.factory.support.AbstractBeanDefinitionReader 抽象类,实现了 BeanDefinitionReader 和 EnvironmentCapable 接口,代码如下:

public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {

	private final BeanDefinitionRegistry registry;

	@Nullable
	private ResourceLoader resourceLoader;

	@Nullable
	private ClassLoader beanClassLoader;

	private Environment environment;

	private BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();

	protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;

		// Determine ResourceLoader to use.
		if (this.registry instanceof ResourceLoader) {
			this.resourceLoader = (ResourceLoader) this.registry;
		}
		else {
			this.resourceLoader = new PathMatchingResourcePatternResolver();
		}

		// Inherit Environment if possible
		if (this.registry instanceof EnvironmentCapable) {
			this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
		}
		else {
			this.environment = new StandardEnvironment();
		}
	}

	@Override
	public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
		Assert.notNull(resources, "Resource array must not be null");
		int count = 0;
		for (Resource resource : resources) {
			count += loadBeanDefinitions(resource);
		}
		return count;
	}

	@Override
	public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
		return loadBeanDefinitions(location, null);
	}

	public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
		// 获得 ResourceLoader 对象
		ResourceLoader resourceLoader = getResourceLoader();
		if (resourceLoader == null) {
			throw new BeanDefinitionStoreException(
					"Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
		}

		if (resourceLoader instanceof ResourcePatternResolver) {
			// Resource pattern matching available.
			try {
				// 获得 Resource 数组,因为 Pattern 模式匹配下,可能有多个 Resource 。例如说,Ant 风格的 location
				Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
				// 加载 BeanDefinition 们
				int count = loadBeanDefinitions(resources);
				if (actualResources != null) {
					// 添加到 actualResources 中
					Collections.addAll(actualResources, resources);
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
				}
				return count;
			}
			catch (IOException ex) {
				throw new BeanDefinitionStoreException(
						"Could not resolve bean definition resource pattern [" + location + "]", ex);
			}
		}
		else {
			// Can only load single resources by absolute URL.
			// 获得 Resource 对象
			Resource resource = resourceLoader.getResource(location);
			// 加载 BeanDefinition 们
			int count = loadBeanDefinitions(resource);
			if (actualResources != null) {
				// 添加到 actualResources 中
				actualResources.add(resource);
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
			}
			return count;
		}
	}

	@Override
	public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
		Assert.notNull(locations, "Location array must not be null");
		int count = 0;
		for (String location : locations) {
			count += loadBeanDefinitions(location);
		}
		return count;
	}
    
    // ... 省略相关代码
}

在实现的方法中,最终都会调用 int loadBeanDefinitions(Resource resource) 这个方法,该方法在子类中实现

XmlBeanDefinitionReader

org.springframework.beans.factory.xml.XmlBeanDefinitionReader,XML 文件资源解析器,解析出 BeanDefinition 配置元信息对象并注册

构造函数

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
	/**
	 * 禁用验证模式
	 */
	public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;

	/**
	 * 自动获取验证模式
	 */
	public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;

	/**
	 * DTD 验证模式
	 */
	public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;

	/**
	 * XSD 验证模式
	 */
	public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;

	/** Constants instance for this class. */
	private static final Constants constants = new Constants(XmlBeanDefinitionReader.class);

	/**
	 * 验证模式,默认为自动模式。
	 */
	private int validationMode = VALIDATION_AUTO;

	private boolean namespaceAware = false;

	private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;

	/**
	 * 解析过程中异常处理器
	 */
	private ProblemReporter problemReporter = new FailFastProblemReporter();

	private ReaderEventListener eventListener = new EmptyReaderEventListener();

	private SourceExtractor sourceExtractor = new NullSourceExtractor();

	@Nullable
	private NamespaceHandlerResolver namespaceHandlerResolver;

	private DocumentLoader documentLoader = new DefaultDocumentLoader();

	@Nullable
	private EntityResolver entityResolver;

	private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);

	/**
	 * XML 验证模式探测器
	 */
	private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();

	/**
	 * 当前线程,正在加载的 EncodedResource 集合。
	 */
	private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded = new NamedThreadLocal<>(
        "XML bean definition resources currently being loaded");

	/**
	 * Create new XmlBeanDefinitionReader for the given bean factory.
	 * @param registry the BeanFactory to load bean definitions into,
	 * in the form of a BeanDefinitionRegistry
	 */
	public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
		super(registry);
	}
}

loadBeanDefinitions 方法

loadBeanDefinitions(Resource resource) 方法,解析 Resource 资源的入口,方法如下:

@Override
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.isTraceEnabled()) {
        logger.trace("Loading XML bean definitions from " + encodedResource);
    }

    // <1> 获取当前线程正在加载的 Resource 资源集合,添加当前 Resource,防止重复加载
    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> 从 Resource 资源获取 InputStream 流对象(支持编码)
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            InputSource inputSource = new InputSource(inputStream);
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            // <3> 【核心】执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        } finally {
            // 关闭流
            inputStream.close();
        }
    } catch (IOException ex) {
        throw new BeanDefinitionStoreException(
                "IOException parsing XML document from " + encodedResource.getResource(), ex);
    } finally {
        // <4> 从当前线程移除当前加载的 Resource 对象
        currentResources.remove(encodedResource);
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}

将Resource 封装成 EncodedResource 对象,目的是让资源对象可设置编码

1、 获取当前线程正在加载的Resource资源集合,添加当前Resource,防止重复加载;
2、 从Resource资源获取InputStream流对象(支持编码);
3、 【核心】调用doLoadBeanDefinitions(InputSourceinputSource,Resourceresource)方法,执行加载Resource资源过程,解析出BeanDefinition进行注册;
4、 从当前线程移除当前加载的Resource对象;

doLoadBeanDefinitions 方法

doLoadBeanDefinitions(InputSource inputSource, Resource resource) 方法,执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册,方法如下:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
        throws BeanDefinitionStoreException {
    try {
        // <1> 获取 XML Document 实例
        Document doc = doLoadDocument(inputSource, resource);
        // <2> 根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量
        int count = registerBeanDefinitions(doc, resource);
        if (logger.isDebugEnabled()) {
            logger.debug("Loaded " + count + " bean definitions from " + resource);
        }
        return count;
    }
    // 省略 catch 各种异常
}

1、 调用doLoadDocument(InputSourceinputSource,Resourceresource)方法,获取XMLDocument实例;
2、 调用registerBeanDefinitions(Documentdoc,Resourceresource)方法,根据Document实例,解析出BeanDefinition们并注册,返回注册数量;

doLoadDocument 方法

doLoadDocument(InputSource inputSource, Resource resource) 方法,获取 Resource 资源对应的 XML Document 实例,方法如下:

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    // <3> 通过 DefaultDocumentLoader 根据 Resource 获取一个 Document 对象
    return this.documentLoader.loadDocument(inputSource,
            getEntityResolver(), // `<1>` 获取 org.xml.sax.EntityResolver 实体解析器,ResourceEntityResolver
            this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware()); // <2> 获取 XML 文件验证模式,保证 XML 文件的正确性
}

1、 获取org.xml.sax.EntityResolver实体解析器,ResourceEntityResolver,根据publicId和systemId获取对应的DTD或XSD文件,用于对XML文件进行验证,这个类比较关键,在后续文章会讲到;
2、 获取XML文件验证模式,保证XML文件的正确性,通常情况下都是XSD模式

1、 获取指定的验证模式,如果手动指定,则直接返回,通常情况下不会;
2、 从Resource资源中获取验证模式,根据XML文件的内容进行获取,如果包含DOCTYPE内容则为DTD模式,否则为XSD模式;
3、 如果还没有获取到验证模式,则默认为XSD模式;
3、 通过DefaultDocumentLoader根据Resource获取一个Document对象;

1、 创建DocumentBuilderFactory对象factory,开启校验;
2、 根据factory创建DocumentBuilder对象builder,设置EntityResolver(第1步创建的)、ErrorHandler属性;
3、 通过builderinputSource(Resource资源)进行解析,返回一个Document对象;

上述过程目的就是获取到 Resource 资源对应的 Document 对象,需要经过校验和解析两个过程

registerBeanDefinitions 方法

registerBeanDefinitions(Document doc, Resource resource) 方法,根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量,方法如下:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // <1> 创建 BeanDefinitionDocumentReader 对象
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // <2> 获取已注册的 BeanDefinition 数量
    int countBefore = getRegistry().getBeanDefinitionCount();
    // <3> 创建 XmlReaderContext 对象(读取 Resource 资源的上下文对象)
    // <4> 根据 Document、XmlReaderContext 解析出所有的 BeanDefinition 并注册
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // <5> 计算新注册的 BeanDefinition 数量
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

1、 创建DefaultBeanDefinitionDocumentReader对象documentReader
2、 获取已注册的BeanDefinition数量;
3、 创建XmlReaderContext对象(读取Resource资源的上下文对象),注意这里会初始化一个DefaultNamespaceHandlerResolver对象,用于处理自定义标签(XML文件),比较关键,在后续文章会讲到;
4. 根据 Document、XmlReaderContext 解析出所有的 BeanDefinition 并注册,调用 DefaultBeanDefinitionDocumentReader#registerBeanDefinitions(Document doc, XmlReaderContext readerContext) 方法
5、 计算新注册的BeanDefinition数量并返回;

拓展:DTD 与 XSD 的区别?

DTD(Document Type Definition),即文档类型定义,为 XML 文件的验证机制,属于 XML 文件中组成的一部分。DTD 是一种保证 XML 文档格式正确的有效验证方式,它定义了相关 XML 文档的元素、属性、排列方式、元素的内容类型以及元素的层次结构。其实 DTD 就相当于 XML 中的 “词汇”和“语法”,我们可以通过比较 XML 文件和 DTD 文件 来看文档是否符合规范,元素和标签使用是否正确。

DTD在一定的阶段推动了 XML 的发展,但是它本身存在着一些缺陷

1、 它没有使用XML格式,而是自己定义了一套格式,相对解析器的重用性较差;而且DTD的构建和访问没有标准的编程接口,导致解析器很难简单的解析DTD文档;
2、 DTD对元素的类型限制较少;同时其他的约束力也比较弱;
3、 DTD扩展能力较差;
4、 基于正则表达式的DTD文档的描述能力有限;

XSD(XML Schemas Definition),即 XML Schema 语言,针对 DTD 的缺陷由 W3C 在 2001 年推出。XML Schema 本身就是一个 XML 文档,使用的是 XML 语法,因此可以很方便的解析 XSD 文档。相对于 DTD,XSD 具有如下优势

1、 XMLSchema基于XML,没有专门的语法;
2、 XMLSchema可以像其他XML文件一样解析和处理;
3、 XMLSchema比DTD提供了更丰富的数据类型;
4、 XMLSchema提供可扩充的数据模型;
5、 XMLSchema支持综合命名空间;
6、 XMLSchema支持属性组;

总结

我们在Spring 中通常以这两种方式定义一个 Bean:面向资源(XML、Properties)面向注解,对于第一种方式如果定义的是一个 XML 文件,Spring 会通过 XmlBeanDefinitionReader 加载该 XML 文件,获取该 Resource 资源的 org.w3c.dom.Document 对象,这个过程会经过校验、解析两个步骤

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址:open in new window