前言
前一篇文章讲述了加载Bean的第一个步骤----XML文件的验证模式,这篇文章将会讲述其第二个步骤,也就是加载XML,得到对应的Document对象。话不多说,开始。
获取Document
经过了验证模式准备的步骤,就可以进行Document加载了。同样的XMLBeanFactoryReader类对于文档的读取没有亲力亲为,而是委托给了DocumentLoader去读取,先来看一下这个接口:
public interface DocumentLoader { Document loadDocument( InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception; }
DocumentLoader中只有一个方法loadDocument(),该方法接收五个参数:
(1)inputSource:加载Document的Resource源。
(2)entityResolver:解析文件的解析器。
(3)errorHandler:处理加载Document对象过程的错误。
(4)validationMode:验证模式。
(5)namespaceAware:命名空间支持。如果要提供对XML名称空间的支持,则为true。
该方法由Document的默认实现类DefaultDocumentLoader实现,具体实现如下:
1 public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, 2 ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { 3 4 DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); 5 if (logger.isTraceEnabled()) { 6 logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]"); 7 } 8 DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); 9 return builder.parse(inputSource); 10 }
首先创建通过createDocumentBuilderFactory方法创建DocumentBuilderFactory,再通过factory创建DocumentBuilder,最后解析inputSource来返回Document对象。
EntityResolver
通过loadDocument()获取Document对象时,有一个参数entityResolver,该参数是通过getEntityResolver()获取的。
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; }
如果ResourceLoader不为null,则根据指定的ResourceLoader创建一个ResourceEntityResolver。如果ResourceLoader为null,则创建一个DelegatingEntityResolver,该Resolver委托给默认的BeansDtdResolver和PluggableSchemaResolver。
❤ ResourceEntityResolver:继承自EntityResolver,通过ResourceLoader来解析实体的引用。
❤ DelegatingEntityResolver:EntityResolver的实现,分别代理了DTD的BeansDtdResolver 和XML Schemas 的 PluggableSchemaResolver。
❤ BeansDtdResolver:Spring中Bean的DTD解析器。EntityResolver的实现,用来从classpath或者jar文件加载DTD。
❤ PluggableSchemaResolver:使用一系列Map文件将Schema URL解析到本地classpath资源。
getEntityResolver()返回EntityResolver,那这个EntityResolver到底是什么呢?
官网是这样解释的:;如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver方法向SAX驱动器注册一个实例。也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证。默认的寻找规则,即通过网络(实现上就是声明的DTD的URL地址)来下载相应的DTD声明,并进行认证。下载的过程漫长,而且当网络中断或不可用的时候,这里会报错,就是因为相应的DTD声明没有被找到的原因。
EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中的某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。
首先来看一下EntityResolver的接口方法声明:
public abstract InputSource resolveEntity (String publicId,String systemId) throws SAXException, IOException;
可以看出,上面方法接收两个参数publicId和systemId,并返回一个inputSource对象。接下来我们以特定的文件来进行讲解。
(1)如果我们在解析验证模式为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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> ...... </beans>
读取到以下两个参数。
publicId:null
systemId:http://www.springframework.org/schema/beans/spring-beans.xsd
(2)如果我们在解析验证模式为DTD的配置文件,代码如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.springframework.org/schema/beans/spring-beans.xsd"> <beans"> ...... </beans>
读取到以下两个参数:
publicId:-//Spring//DTD BEAN 2.0//EN
systemId:http://www.springframework.org/schema/beans/spring-beans.xsd
之前已经说过,验证模式的加载方式是通过URL进行网络下载的,这样会造成延时,用户体验也不好,一般的做法是将验证文件放置在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件呢?我们以加载DTD文件为例来看看Spring中是如何实现的。根据之前Spring中通过getEntityResolver()方法对EntityResolver的获取,我们知道,Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolverEntity实现方法如下:
public InputSource resolveEntity(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); } } return null; }
我们可以看出,对不同的验证模式,Spring采用了不同的解析器进行解析。简单的描述一下原理,比如加载DTD类型的BeanDtdResolver的resolverEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemId所对应的XSD文件并加载。下面是BeansDtdResolver的源码:
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; }
从上面的代码中我们可以看出加载DTD类型的BeansDtdResolver.resolveEntity()只是对systemId进行了简单的校验(从最后一个“/”开始,内容中是否包含spring-beans),然后构造一个InputSource并设置publicId、systemId,然后返回。
PluggableSchemaResolver 的解析过程如下:
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) { String resourceLocation = getSchemaMappings().get(systemId); 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.isDebugEnabled()) { logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); } return source; } catch (FileNotFoundException ex) { if (logger.isDebugEnabled()) { logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex); } } } } return null; }
首先调用 getSchemaMappings() 获取一个映射表(systemId 与其在本地的对照关系),然后根据传入的 systemId 获取该 systemId 在本地的路径 resourceLocation,最后根据 resourceLocation 构造 InputSource 对象,并设置publicId、systemId,然后返回。
参考:《Spring源码深度解析》 郝佳 编著: