ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【死磕 Spring】—— IoC 之获取 Document 对象

2022-04-19 11:00:44  阅读:159  来源: 互联网

标签:publicId String Spring InputSource systemId EntityResolver logger Document IoC


目录

前言

XmlBeanDefinitionReader#document(InputSource inputSource,Resource resource) 方法中,主要做了两件事:

  • 调用 \#getValidationModeForResource(Resource resource) 获取指定资源xml的验证模式。上篇博文中已经做了详细的解析。
  • 调用DocumentLoader#loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) 方法,获取 XML Document 实例。

1 DocumentLoader

​ 获取 Document 的策略,有接口,org.springframework.beans.factory.xml.DocumentLoader 定义。代码如下:

定义从资源文件加载到转换为 Document 的功能。

public interface DocumentLoader {

	Document loadDocument(
			InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware)
			throws Exception;

}
  • inputSource 方法参数,加载 Document 的 Resource 资源。
  • entityResolver 方法参数,解析文件的解析器。
  • errorHandler 方法参数,处理加载 Document 对象的过程的错误。
  • validationMode 方法参数,验证模式。
  • namespaceAware 方法参数,命名空间支持。如果要提供对 XML 名称空间的支持,则需要值为 true

1.1 DefauleDocumentLoader

该方法有DocumentLoader 的默认实现类,org.springframework.beans.factory.xml.DefaultDocumentLoader实现。代码如下:

/**
 * Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured
 * XML parser.
 */
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
	// <1> 创建 DocumentBuilderFactory
	DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
	if (logger.isTraceEnabled()) {
		logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
	}
	// <2> 创建 DocumentBuilder
	DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
	// <3> 解析 XML InputSource 返回 Document 对象
	return builder.parse(inputSource);
}
  • 首先,调用 #createDocumentBuilderFactory(...) 方法,创建javax.xml.parsers.DocumentBuilderFactory 对象。代码如下:
/**
 * JAXP attribute used to configure the schema language for validation.
 */
private static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
/**
 * JAXP attribute value indicating the XSD schema language.
 */
private static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema";
protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware) throws ParserConfigurationException {
    // 创建 DocumentBuilderFactory
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(namespaceAware); // 设置命名空间支持
    if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
        factory.setValidating(true); // 开启校验
        // XSD 模式下,设置 factory 的属性
        if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
            // Enforce namespace aware for XSD...
            factory.setNamespaceAware(true); // XSD 模式下,强制设置命名空间支持
            // 设置 SCHEMA_LANGUAGE_ATTRIBUTE
            try {
                factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
            } catch (IllegalArgumentException ex) {
                ParserConfigurationException pcex = new ParserConfigurationException(
                        "Unable to validate using XSD: Your JAXP provider [" + factory +
                        "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
                        "Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
                pcex.initCause(ex);
                throw pcex;
            }
        }
    }
    return factory;
}
  • 然后,调用#createDocumentBuilder(DocumentBuilderFactory factory, EntityResolver entityResolver,ErrorHandler errorHandler) 方法,创建 javax.xml.parsers.DocumentBuilder 对象。代码如下:
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
		@Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
		throws ParserConfigurationException {
	// 创建 DocumentBuilder 对象
	DocumentBuilder docBuilder = factory.newDocumentBuilder();
	// <x> 设置 EntityResolver 属性
	if (entityResolver != null) {
		docBuilder.setEntityResolver(entityResolver);
	}
	// 设置 ErrorHandler 属性
	if (errorHandler != null) {
		docBuilder.setErrorHandler(errorHandler);
	}
	return docBuilder;
}

​ 在 处,设置DocumentBuild 的EntityResolver 属性。关于它,在【2】会详细解析。

  • 最后,调用 DocumentBuilder#parse(InputSource) 方法,解析 InputSource ,返回 Document 对象

2 EntityResolver

​ 通过 DocumentLoader#loadDocument(...) 方法来获取Document 对象,有一个方法参数entityResolver。该参数是通过,XmlBeanDefinitionReader#getEntityResolver() 方法来获取的。代码如下:

#getEntityResolver() 方法,返回指定的解析器,如果没有指定,则构造一个未指定的默认解析器。

// XmlBeanDefinitionReader.java

/**
 * EntityResolver 解析器
 */
@Nullable
private EntityResolver entityResolver;

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;
}
  • 如果ResourceLoader 不为null , 则根据指定的ResourceLoader 创建一个ResourceEntityResolver 对象。
  • 如果ResourceLoader 为null,则创建一个 DelegatingEntityResolver 对象,该 Resolver 委托给默认的 BeansDtdResolver 和 PluggableSchemaResolver 。

2.1 子类

上面的方法,一共涉及四个EntityResolver 的子类:

  • org.springframework.beans.factory.xm.beansDtdResolver: 实现EntityResolver接口,Spring Bean dtd 解码器,用来从 classpath 或者 jar 文件中加载 dtd 。部分代码如下:
private static final String DTD_EXTENSION = ".dtd";

private static final String DTD_NAME = "spring-beans";
  • org.springframework.beans.factory.xml.PluggableSchemaResolver ,实现 EntityResolver 接口,读取 classpath 下的所有 "META-INF/spring.schemas" 成一个 namespaceURI 与 Schema 文件地址的 map 。代码如下:
/**
 * The location of the file that defines schema mappings.
 * Can be present in multiple JAR files.
 *
 * 默认 {@link #schemaMappingsLocation} 地址
 */
public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";

@Nullable
private final ClassLoader classLoader;

/**
 * Schema 文件地址
 */
private final String schemaMappingsLocation;

/** Stores the mapping of schema URL -> local schema path. */
@Nullable
private volatile Map<String, String> schemaMappings; // namespaceURI 与 Schema 文件地址的映射集合
  • org.springframework.beans.factory.xml.DelegatingEntityResolver :实现 EntityResolver 接口,分别代理 dtd 的 BeansDtdResolver 和 xml schemas 的 PluggableSchemaResolver 。代码如下:
/** Suffix for DTD files. */
public static final String DTD_SUFFIX = ".dtd";

/** Suffix for schema definition files. */
public static final String XSD_SUFFIX = ".xsd";

private final EntityResolver dtdResolver;

private final EntityResolver schemaResolver;

// 默认
public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
	this.dtdResolver = new BeansDtdResolver();
	this.schemaResolver = new PluggableSchemaResolver(classLoader);
}

// 自定义
public DelegatingEntityResolver(EntityResolver dtdResolver, EntityResolver schemaResolver) {
	Assert.notNull(dtdResolver, "'dtdResolver' is required");
	Assert.notNull(schemaResolver, "'schemaResolver' is required");
	this.dtdResolver = dtdResolver;
	this.schemaResolver = schemaResolver;
}
  • org.springframework.beans.factory.xml.ResourceEntityResolver :继承自 DelegatingEntityResolver 类,通过 ResourceLoader 来解析实体的引用。代码如下:
private final ResourceLoader resourceLoader;

public ResourceEntityResolver(ResourceLoader resourceLoader) {
	super(resourceLoader.getClassLoader());
	this.resourceLoader = resourceLoader;
}

2.2 作用

EntityResolver 的作用就是,通过实现它,应用可以自定义如何寻找【验证文件】的逻辑。

org.xml.sax.EntityResolver 接口,代码如下:

public interface EntityResolver {

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

}

接口方法接收两个参数 publicIdsystemId ,并返回 InputSource 对象。两个参数声明如下:

  • publicId :被引用的外部实体的公共标识符,如果没有提供,则返回 null
  • systemId :被引用的外部实体的系统标识符。

这两个参数的实际内容和具体的验证模式的关系如下:

2.3 DelegatingEntityResolver

​ 我们知道在 Spring 中使用 DelegatingEntityResolver 为 EntityResolver 的实现类。#resolveEntity(String publicId, String systemId) 方法,实现如下:

@Override
@Nullable
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
	if (systemId != null) {
		// DTD 模式
		if (systemId.endsWith(DTD_SUFFIX)) {
			return this.dtdResolver.resolveEntity(publicId, systemId);
		// XSD 模式
		} else if (systemId.endsWith(XSD_SUFFIX)) {
			return this.schemaResolver.resolveEntity(publicId, systemId);
		}
	}
	return null;
}
  • 如果是 DTD 验证模式,则使用 BeansDtdResolver 来进行解析
  • 如果是 XSD 验证模式,则使用 PluggableSchemaResolver 来进行解析。

2.4 BeansDtdResolver

BeansDtdResolver 的解析过程,代码如下:

/**
 * DTD 文件的后缀
 */
private static final String DTD_EXTENSION = ".dtd";
/**
 * Spring Bean DTD 的文件名
 */
private static final String DTD_NAME = "spring-beans";

@Override
@Nullable
public InputSource resolveEntity(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 + "]");
    }
    // 必须以 .dtd 结尾
    if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
        // 获取最后一个 / 的位置
        int lastPathSeparator = systemId.lastIndexOf('/');
        // 获取 spring-beans 的位置
        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 {
                // 创建 ClassPathResource 对象
                Resource resource = new ClassPathResource(dtdFile, getClass());
                // 创建 InputSource 对象,并设置 publicId、systemId 属性
                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 (IOException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
                }
            }
        }
    }

    // 使用默认行为,从网络上下载
    // Use the default behavior -> download from website or wherever.
    return null;
}

从上面的代码中,我们可以看到,加载 DTD 类型的 BeansDtdResolver#resolveEntity(...) 过程,只是对 systemId 进行了简单的校验(从最后一个 / 开始,内容中是否包含 spring-beans),然后构造一个 InputSource 对象,并设置 publicIdsystemId 属性,然后返回。

2.5 PluggableSchemaResolver

PluggableSchemaResolver 的解析过程,代码如下:

@Nullable
private final ClassLoader classLoader;

/**
 * Schema 文件地址
 */
private final String schemaMappingsLocation;

/** Stores the mapping of schema URL -> local schema path. */
@Nullable
private volatile Map<String, String> schemaMappings; // namespaceURI 与 Schema 文件地址的映射集合

@Override
@Nullable
public InputSource resolveEntity(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) {
        // 获得 Resource 所在位置
        String resourceLocation = getSchemaMappings().get(systemId);
        if (resourceLocation != null) {
            // 创建 ClassPathResource
            Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
            try {
                // 创建 InputSource 对象,并设置 publicId、systemId 属性
                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);
                }
            }
        }
    }
    return null;
}
  • 首先调用 #getSchemaMappings() 方法,获取一个映射表(systemId 与其在本地的对照关系)。代码如下:

    private Map<String, String> getSchemaMappings() {
        Map<String, String> schemaMappings = this.schemaMappings;
        // 双重检查锁,实现 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 的方式,读取 schemaMappingsLocation
                        Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
                        if (logger.isTraceEnabled()) {
                            logger.trace("Loaded schema mappings: " + mappings);
                        }
                        // 将 mappings 初始化到 schemaMappings 中
                        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;
    }
    
    • 映射表如下(部分):映射表
  • 然后,根据传入的 systemId 获取该 systemId 在本地的路径 resourceLocation

  • 最后,根据 resourceLocation ,构造 InputSource 对象。

2.6 ResourceEntityResolver

ResourceEntityResolver 的解析过程,代码如下:

private final ResourceLoader resourceLoader;

@Override
@Nullable
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
    // 调用父类的方法,进行解析
    InputSource source = super.resolveEntity(publicId, systemId);
    // 解析失败,resourceLoader 进行解析
    if (source == null && systemId != null) {
        // 获得 resourcePath ,即 Resource 资源地址
        String resourcePath = null;
        try {
            String decodedSystemId = URLDecoder.decode(systemId, "UTF-8"); // 使用 UTF-8 ,解码 systemId
            String givenUrl = new URL(decodedSystemId).toString(); // 转换成 URL 字符串
            // 解析文件资源的相对路径(相对于系统根路径)
            String systemRootUrl = new File("").toURI().toURL().toString();
            // Try relative to resource base if currently in system root.
            if (givenUrl.startsWith(systemRootUrl)) {
                resourcePath = givenUrl.substring(systemRootUrl.length());
            }
        } catch (Exception ex) {
            // Typically a MalformedURLException or AccessControlException.
            if (logger.isDebugEnabled()) {
                logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex);
            }
            // No URL (or no resolvable URL) -> try relative to resource base.
            resourcePath = systemId;
        }
        if (resourcePath != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]");
            }
            // 获得 Resource 资源
            Resource resource = this.resourceLoader.getResource(resourcePath);
            // 创建 InputSource 对象
            source = new InputSource(resource.getInputStream());
            // 设置 publicId 和 systemId 属性
            source.setPublicId(publicId);
            source.setSystemId(systemId);
            if (logger.isDebugEnabled()) {
                logger.debug("Found XML entity [" + systemId + "]: " + resource);
            }
        }
    }
    return source;
}
  • 首先,调用父类的方法,进行解析。
  • 如果失败,使用 resourceLoader ,尝试读取 systemId 对应的 Resource 资源。

2.7 自定义 EntityResolver

#getEntityResolver() 方法返回 EntityResolver 对象。那么怎么进行自定义 EntityResolver 呢?

If a SAX application needs to implement customized handling for external entities, it must implement this interface and register an instance with the SAX driver using the setEntityResolver method.

就是说:如果 SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口,并使用 #setEntityResolver(EntityResolver entityResolver) 方法,向 SAX 驱动器注册一个 EntityResolver 实例。

示例如下:

public class MyResolver implements EntityResolver {

    @Override
    public InputSource resolveEntity(String publicId, String systemId) {
        if (systemId.equals("http://www.myhost.com/today")) {
            MyReader reader = new MyReader();
            return new InputSource(reader);
        } else {
            // use the default behaviour
            return null;
        }
    }

}

我们首先将 "spring-student.xml" 文件中的 XSD 声明的地址改掉,如下:

spring-student.xml

如果我们再次运行,则会报如下错误:

报错

从上面的错误可以看到,是在进行文档验证时,无法根据声明找到 XSD 验证文件而导致无法进行 XML 文件验证。在 《【死磕 Spring】—— IoC 之获取验证模型》 中讲到,如果要解析一个 XML 文件,SAX 首先会读取该 XML 文档上的声明,然后根据声明去寻找相应的 DTD 定义,以便对文档进行验证。默认的加载规则是通过网络方式下载验证文件,而在实际生产环境中我们会遇到网络中断或者不可用状态,那么就应用就会因为无法下载验证文件而报错。

标签:publicId,String,Spring,InputSource,systemId,EntityResolver,logger,Document,IoC
来源: https://www.cnblogs.com/zhaoqiang-lab/p/16164218.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有