概述
这一篇开始进行 Document 加载了,XmlBeanFactoryReader 类对于文档读取并没有亲历亲为,而是委托给了DocumentLaoder 去执行,DocumentLoader 是个接口,真正调用的是 DefaultDocumentLoader。
解析代码
/*** Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured* XML parser.*/@Overridepublic Document loadDocument(InputSource inputSource, EntityResolver entityResolver,ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);if (logger.isDebugEnabled()) {logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");}DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);return builder.parse(inputSource);}
对于这部分代码其实并没有太多可以描述的,因为通过 SAX 解析 XML 文档的套路都差不多,Spring 在这里并没有什么特殊的地方,同样首先创建 DocumentBuilderFactory,再通过 DocumentBuilderFactory 创建DocumentBuilder,进而解析 inputSource 来返回 Document 对象。这里有必要提及一下 EntityResolver,对于参数 entityResolver,传入的是通过getEntityResolver() 函数获取的返回值,如下代码:
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;}
那么,EntityResolver 到底是做什么用的呢?
EntityResolver 用法
在 loadDocument 方法中涉及一个参数 EntityResolver,何为 EntitiResolver?官网这样解释:如果 SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口并使用 setEntityResolver 方法向 SAX 驱动器注册一个实例。也就是说,对于解析一个 XML,SAX 首先读取该 XML 文档上的声明,根据声明去寻找相应的 DTD 定义,以便对文档进行一个验证。默认的寻找规则,即通过网络(实现上就是声明的 DTD 的 URL 地址)来下载相应的 DTD 声明,并进行认证。下载的过程漫长,而且当网络中断或不可用的时候,这里会报错,就是因为相应的 DTD 声明没有被找到的原因。
enntityResolver 的作用是项目本身就可以提供一个如何寻找 DTD 声明的方法,即由程序来实现寻找 DTD 声明的过程,比如我们将 DTD 文件放到项目中某处,在实现时直接将此文档读取并返回给 SAX 即可。这样就避免了通过网络来寻找相应的声明。
首先看enntityResolver的接口方法声明:
public abstract InputSource resolveEntity (String publicId, String systemId)throws SAXException, IOException;
这里,它接受两个参数 publicId 和 systemId ,并返回一个 inputSource 对象。这里我们以特定配置文件来进行讲解。
(1)如果我们在解析验证模式为XSD的配置文件,代码如下:
读取到以下两个参数。
- publicId:null
- systemId:http://www.springframework.org/schema/beans/spring-beans.xsd
(2)如果我们在解析验证模式为DTD的配置文件,代码如下:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.dtd">......</beans>
读取到以下两个参数:
- publicId:-//Spring//DTD BEAN 2.0//EN
- systemId:http://www.springframework.org/schema/beans/spring-beans.dtd
之前已经提到过,验证文件默认的加载方式是通过URL进行网络下载,这样会造成延时,用户体验也不好,一般的做法是将验证文件放置在自己的工程里,那么怎么做才能将这个 URL 转换为自己工程里对应的地址文件呢?我们以加载 DTD 文件为例来看看 Spring 中是如何实现的。根据之前 Spring 中通过 getEntityResolver() 方法对EntityResolver 的获取,我们知道,Spring 中使用 DelegatingEntityResolver 类为 EntityResolver 的实现类,resolveEntity实现方法如下:
@Override@Nullablepublic InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {if (systemId != null) {if (systemId.endsWith(DTD_SUFFIX)) {// 如果是dtd从这里解析return this.dtdResolver.resolveEntity(publicId, systemId);}else if (systemId.endsWith(XSD_SUFFIX)) {// 通过调用META-INF/Spring.schemas解析return this.schemaResolver.resolveEntity(publicId, systemId);}}return null;}
我们可以看到,对不同的验证模式,Spring 使用了不同的解析器解析。这里简单描述一下原理,比如加载 DTD 类型的 BeanDtdResolver 的 resolveEntity 是直接截取 systemId 最后的 xx.dtd 然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver 类的resolveEntity 是默认到 META-INF/Spring.schemas 文件中找到 systemId 所对应的 XSD 文件并加载。下面是 BeansDtdResolver 的源码:
@Override@Nullablepublic 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 && 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.isDebugEnabled()) {logger.debug("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;}
