概述
这一篇开始进行 Document 加载了,XmlBeanFactoryReader 类对于文档读取并没有亲历亲为,而是委托给了DocumentLaoder 去执行,DocumentLoader 是个接口,真正调用的是 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 {
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/beans
http://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
@Nullable
public 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
@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 && 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;
}