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资源文件的输入流,然后交给XmlValidationModeDetector
的detectValidationMode(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解析工具来完成了。