Spring XML默认标签配置解析注册Bean源码分析(三)——Document对象

3.3.1. Document对象

        我们之前说在BeanDefinition的解析注册过程中,需要将Resource文件资源转换为Document对象,这一节我们就来看一下Document doc = doLoadDocument(inputSource, resource);

// 源码位置:org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadDocument
/**
 * 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());
}
3.3.1.1. getValidationModeForResource

        在解析XML之前,我们首先需要对XML内容及格式进行验证,XML验证常用的模式有DTD和XSD两种。

DTD验证模式:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd" >

        我们可以把DTD(Document Type Definition)文件下载下来或在Spring的jar包spring-beans-5.1.8.RELEASE.jar!/org/springframework/beans/factory/xml/下找到spring-beans.dtd文件。查看内容我们发现,DTD文件用来定义XML,它使用一系列合法的元素来定义文档的结构。

XSD验证模式:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

        我们可以通过网络在线查看XSD(XML Schema Definition)文件内容(点击查看)或在Spring的jar包中spring-beans-5.1.8.RELEASE.jar!/org/springframework/beans/factory/xml/spring-beans.xsd查看。XSD的作用也是定义一个XML文档结构,和DTD的作用一样,由于其比DTD更强大,可以替代DTD。

        我们在使用XSD定义XML文档的时候,我们需要在XML中声明:

  • 命名空间:
    xmlns="http://www.springframework.org/schema/beans"
  • 该命名空间对应的XSD位置:
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"它包括两部分,前一部分是命名空间的URI,后一部分是该命名空间所对应的XSD文件位置或URL位置。

上述不论是DTD还是XSD,我们不对其内容和使用做过多的解释,我们只需要知道,他们都是用来定义XML文档结构的语言,Spring将用他们来验证XML的合法性。

        Spring通过getValidationModeForResource(resource)判断文档的验证模式是DTD还是XSD。

// 源码位置:org.springframework.beans.factory.xml.XmlBeanDefinitionReader#getValidationModeForResource
/**
 * Determine the validation mode for the specified {@link Resource}.
 * If no explicit validation mode has been configured, then the validation
 * mode gets {@link #detectValidationMode detected} from the given resource.
 * <p>Override this method if you would like full control over the validation
 * mode, even when something other than {@link #VALIDATION_AUTO} was set.
 * @see #detectValidationMode
 */
protected int getValidationModeForResource(Resource resource) {
	// 如果指定了验证模式,则直接返回
	int validationModeToUse = getValidationMode();
	if (validationModeToUse != VALIDATION_AUTO) {
		return validationModeToUse;
	}
	// 否则继续判断验证模式
	int detectedMode = detectValidationMode(resource);
	if (detectedMode != VALIDATION_AUTO) {
		return detectedMode;
	}
	// 如果判断后仍然返回了VALIDATION_AUTO,那么尝试使用XSD模式验证
	// Hmm, we didn't get a clear indication... Let's assume XSD,
	// since apparently no DTD declaration has been found up until
	// detection stopped (before finding the document's root tag).
	return VALIDATION_XSD;
}

        我们对int detectedMode = detectValidationMode(resource);继续展开。在这里,Spring将通过resource.getInputStream()获取到XML资源文件的输入流,然后交给XmlValidationModeDetectordetectValidationMode(InputStream inputStream)方法做实际判断操作(XmlValidationModeDetector类就是专职做这个的)。

// 源码位置:org.springframework.util.xml.XmlValidationModeDetector#detectValidationMode
/**
 * Detect the validation mode for the XML document in the supplied {@link InputStream}.
 * Note that the supplied {@link InputStream} is closed by this method before returning.
 * @param inputStream the InputStream to parse
 * @throws IOException in case of I/O failure
 * @see #VALIDATION_DTD
 * @see #VALIDATION_XSD
 */
public int detectValidationMode(InputStream inputStream) throws IOException {
	// Peek into the file to look for DOCTYPE.
	BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
	try {
		boolean isDtdValidated = false;
		String content;
		while ((content = reader.readLine()) != null) {
			content = consumeCommentTokens(content);
			if (this.inComment || !StringUtils.hasText(content)) {
				continue;
			}
			// 是否包含“DOCTYPE”关键字,包含就是DTD模式
			if (hasDoctype(content)) {
				isDtdValidated = true;
				break;
			}
			// 标签“<”符号的下一个字符是否是字母,如果是字母则返回true
			if (hasOpeningTag(content)) {
				// End of meaningful data...
				break;
			}
		}
		return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
	}
	catch (CharConversionException ex) {
		// Choked on some character encoding...
		// Leave the decision up to the caller.
		return VALIDATION_AUTO;
	}
	finally {
		reader.close();
	}
}

         上面的代码我们很容易看出Spring的校验逻辑,就是看XML中是否包含“DOCTYPE”关键字,如果包含就是DTD,否则就是XSD。

3.3.1.2. getEntityResolver

         getEntityResolver()方法返回一个EntityResolver对象,他是解析实体的基本接口。根据EntityResolver接口源码中的注释说明,如果 SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver方法(比如DocumentBuilder类中的setEntityResolver方法)向 SAX 驱动器注册一个实例。然后 XML 读取器将允许应用程序在包含外部实体之前截取任何外部实体(包括外部 DTD 子集和外部参数实体,如果有)。许多 SAX 应用程序不需要实现此接口,但对于从数据库或其他特定的输入源中构建 XML 文档的应用程序,或者对于使用 URI 类型(而不是 URL )的应用程序,这特别有用。以上是官方的解释,就是说,对于解析一个XML,SAX首先读取的是XML文档上的声明,根据声明寻找相应的DTD/XSD定义,以便对文档进行校验。默认是通过网络来下载相应的DTD/XSD声明(DTD请参考XML的<!DOCTYPE部分,XSD请参考XML的xsi:schemaLocation部分),并进行验证,但是当网络不通畅的时候就会报错。Spring实现了EntityResolver接口,其目的就是提供一个在本地寻找DTD/XSD的方法,以避免网络寻找所代理的弊端。下面我们看一下具体的实现。

public abstract InputSource resolveEntity (String publicId,
                                           String systemId)
    throws SAXException, IOException;

        EntityResolver接口只用一个方法InputSource resolveEntity (String publicId, String systemId),两个参数:

  • publicId:外部实体的公共标识符,如果没有提供,则为NULL。
  • systemId:要引用的外部实体的系统标识符。
  • return InputSource:返回一个InputSource对象,即本地DTD/XSD文档。

publicId举例:
DTD模式 = -//Spring//DTD BEAN 2.0//EN
XSD模式 = NULL


systemId举例:
DTD模式 = http://www.springframework.org/dtd/spring-beans-2.0.dtd
XSD模式 = http://www.springframework.org/schema/beans/spring-beans.xsd

        随后创建ResourceEntityResolver对象:

// 源码位置:org.springframework.beans.factory.xml.XmlBeanDefinitionReader#getEntityResolver
/**
 * Return the EntityResolver to use, building a default resolver
 * if none specified.
 */
protected EntityResolver getEntityResolver() {
	if (this.entityResolver == null) {
		// Determine default EntityResolver to use.
		ResourceLoader resourceLoader = getResourceLoader();
		if (resourceLoader != null) {
			this.entityResolver = new ResourceEntityResolver(resourceLoader);
		}
		else {
			this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
		}
	}
	return this.entityResolver;
}

        ResourceEntityResolver继承至DelegatingEntityResolver,DelegatingEntityResolver实现了EntityResolver接口,ResourceEntityResolver类对父类InputSource resolveEntity (String publicId, String systemId)方法进行了重写:

// 源码位置:org.springframework.beans.factory.xml.ResourceEntityResolver#resolveEntity
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
		throws SAXException, IOException {

	// 这里调用了父类的resolveEntity方法。
	InputSource source = super.resolveEntity(publicId, systemId);

	if (source == null && systemId != null) {
		// 此处省略。。。
	}
	return source;
}

        我们可以看到InputSource source = super.resolveEntity(publicId, systemId);实际的InputSource返回由其父类完成。

// 源码位置:org.springframework.beans.factory.xml.DelegatingEntityResolver#resolveEntity
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
		throws SAXException, IOException {

	if (systemId != null) {
		if (systemId.endsWith(DTD_SUFFIX)) {
			return this.dtdResolver.resolveEntity(publicId, systemId);
		}
		else if (systemId.endsWith(XSD_SUFFIX)) {
			return this.schemaResolver.resolveEntity(publicId, systemId);
		}
	}

	// Fall back to the parser's default behavior.
	return null;
}

        在这里我们终于看到了DTD和XSD不同模式下的不同处理。

dtdResolver = new BeansDtdResolver();// DTD模式
schemaResolver = new PluggableSchemaResolver(classLoader);// XSD模式
// 它们都实现EntityResolver接口

由于BeansDtdResolver和PluggableSchemaResolver都实现EntityResolver接口,必然有InputSource resolveEntity (String publicId, String systemId)方法,我们只需要看着两个类是如何实现的该方法,就清楚DTD/XSD模式是如何找到本地的定义文档的。

// 源码位置org.springframework.beans.factory.xml.BeansDtdResolver#resolveEntity

private static final String DTD_EXTENSION = ".dtd";

private static final String DTD_NAME = "spring-beans";

@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
	if (logger.isTraceEnabled()) {
		logger.trace("Trying to resolve XML entity with public ID [" + publicId +
				"] and system ID [" + systemId + "]");
	}

	if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
		int lastPathSeparator = systemId.lastIndexOf('/');
		int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
		if (dtdNameStart != -1) {
			String dtdFile = DTD_NAME + DTD_EXTENSION;
			if (logger.isTraceEnabled()) {
				logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
			}
			try {
				Resource resource = new ClassPathResource(dtdFile, getClass());
				InputSource source = new InputSource(resource.getInputStream());
				source.setPublicId(publicId);
				source.setSystemId(systemId);
				if (logger.isTraceEnabled()) {
					logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
				}
				return source;
			}
			catch (FileNotFoundException ex) {
				if (logger.isDebugEnabled()) {
					logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
				}
			}
		}
	}

	// Fall back to the parser's default behavior.
	return null;
}

        上面是DTD模式,根据String dtdFile = DTD_NAME + DTD_EXTENSION;我知道是BeansDtdResolver类所在目录下找spring-beans.dtd文件。

@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
	if (logger.isTraceEnabled()) {
		logger.trace("Trying to resolve XML entity with public id [" + publicId +
				"] and system id [" + systemId + "]");
	}

	if (systemId != null) {
		String resourceLocation = getSchemaMappings().get(systemId);
		if (resourceLocation == null && systemId.startsWith("https:")) {
			// Retrieve canonical http schema mapping even for https declaration
			resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
		}
		if (resourceLocation != null) {
			Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
			try {
				InputSource source = new InputSource(resource.getInputStream());
				source.setPublicId(publicId);
				source.setSystemId(systemId);
				if (logger.isTraceEnabled()) {
					logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
				}
				return source;
			}
			catch (FileNotFoundException ex) {
				if (logger.isDebugEnabled()) {
					logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
				}
			}
		}
	}

	// Fall back to the parser's default behavior.
	return null;
}

        上面是XSD模式,从String resourceLocation = getSchemaMappings().get(systemId);中我们可以看到它是从一个映射关系中,根据systemId找到本地的XSD定义文档。那么我们看一个这映射关系从哪里来。

// 源码位置:org.springframework.beans.factory.xml.PluggableSchemaResolver#resolveEntity

public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";

public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
	this.classLoader = classLoader;
	this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
}

/**
 * Load the specified schema mappings lazily.
 */
private Map<String, String> getSchemaMappings() {
	Map<String, String> schemaMappings = this.schemaMappings;
	if (schemaMappings == null) {
		synchronized (this) {
			schemaMappings = this.schemaMappings;
			if (schemaMappings == null) {
				if (logger.isTraceEnabled()) {
					logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
				}
				try {
					Properties mappings =
							PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
					if (logger.isTraceEnabled()) {
						logger.trace("Loaded schema mappings: " + mappings);
					}
					schemaMappings = new ConcurrentHashMap<>(mappings.size());
					CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
					this.schemaMappings = schemaMappings;
				}
				catch (IOException ex) {
					throw new IllegalStateException(
							"Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
				}
			}
		}
	}
	return schemaMappings;
}

        getSchemaMappings()去加载systemId与本地XSD定义文档的映射关系。Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);这里schemaMappingsLocation=META-INF/spring.schemas,因此我们找到了配置映射关系的文件在META-INF/spring.schemas里面,打开该文件我们看到了映射关系:

http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.1.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.2.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.0.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.1.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
... ...
... ...
... ...略
3.3.1.3. Document对象的加载

        回到开篇的代码片段:

// 源码位置:org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadDocument
/**
 * 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());
}

        我们已经清楚了加载Document对象的关键参数getValidationModeForResource(resource)getEntityResolver(),上面的this.documentLoader是DefaultDocumentLoader对象,通过调用其loadDocument方法,最终我们完成了Document对象产生的工作。

总结Spring中XML资源配置转Document对象的主要工作,一是判断DTD/XSD验证模式;二是这两种模式下在本地能找到DTD/XSD定义文档,摆脱网络访问造成的脱机问题; 剩下的工作就交给JDK的XML解析工具来完成了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值