概述
LoadDocument源码分析
EntityResolver分析
LoadDocument源码分析: 在XmlBeanDefinitionReader.doLoadDocument()
方法中做了两件事情,一是调用 getValidationModeForResource()
获取 XML 的验证模式,二是调用 DocumentLoader.loadDocument()
获取 Document 对象。上篇博客已经分析了获取 XML 验证模式,这篇我们分析获取 Document 对象。
获取 Document 的策略由接口 DocumentLoader 定义,如下:
1 2 3 4 5 6 7 public interface DocumentLoader { Document loadDocument ( InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception; }
DocumentLoader 中只有一个方法 loadDocument()
,该方法接收五个参数:
inputSource:加载 Document 的 Resource 源
entityResolver:解析文件的解析器
errorHandler:处理加载 Document 对象的过程的错误
validationMode:验证模式
namespaceAware:命名空间支持。如果要提供对 XML 名称空间的支持,则为true
该方法由 DocumentLoader 的默认实现类 DefaultDocumentLoader 实现,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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在这里并没有什么特殊的地方。首先调用 createDocumentBuilderFactory()
创建 DocumentBuilderFactory ,再通过该 factory创建DocumentBuilder,最后解析InputSource返回Document对象。不过你如果感兴趣可以自己了解一下。
EntityResolver分析 通过 loadDocument()
获取 Document 对象时,有一个参数 entityResolver ,该参数是通过 getEntityResolver()
获取的。何为EntityResolver?官网这样解释,如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityReslover方法想SAX驱动器注册一个实例。也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的XSD定义,以便对该文档进行一个验证。默认的寻找规则及通过网络(实现上就是声明的XSD的URI地址)来下载相应的XSD声明,并进行验证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的XSD声明没有被找到的原因。
EntityResolver的作用是项目本省就可以提供一个如何寻找XSD声明的方法,即有程序来实现寻找XSD声明的过程,比如我们将XSD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。
首先看entityResolver的接口的方法声明:
InputSource resolveEntity(String publiId,String systemId)
这里,他接受俩个参数publicId和systemId,返回一个InputSource,这个对象不是我们之前介绍的统一资源那个对象,是org.xml.sax.InputSource
这个对象。下面我们以特定的例子来讲解
如果我们在解析验证模式为XSD的配置文件,代码如下
1 2 3 4 5 6 7 <?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 >
读到以下俩个参数
如果我们在解析验证模式为DTD的配置文件,代码如下
1 2 <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd" >
读取到以下来个参数:
之前已经提到过,验证文件默认的加载方式是通过URL进行网络下载获取,这样会造成延迟,用户体验也不好,一般的做法都是将验证文件防止在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件呢?我们已加载DTD文件为例来看看Spring中是如何实现的。下面先分析getEntityResolver
,然后具体讲解一下EntityResolver的实现方式
getEntityResolver()
返回指定的解析器,如果没有指定,则构造一个未指定的默认解析器。
1 2 3 4 5 6 7 8 9 10 11 12 13 protected EntityResolver getEntityResolver () { if (this .entityResolver == null ) { 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 。
我们就拿DelegatingEntityResolver来具体分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/spring.schemas
文件中找到systemId所对应的XSD文件并加载。
BeansDtdResolver 的解析过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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); } } } } return null ; }
从上面的代码中我们可以看到加载 DTD 类型的 BeansDtdResolver.resolveEntity()
只是对 systemId 进行了简单的校验(从最后一个 / 开始,内容中是否包含 spring-beans
),然后构造一个 InputSource 并设置 publicId、systemId,然后返回。
PluggableSchemaResolver 的解析过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 对象。
下面是getSchemaMappings源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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; }
从上面可以看到,先去加载指定目录下的所有XSD键值对,然后和当前已经存在XSD键值对合并,然后返回一个ConcurrentHashMap来方便查找同时保证了线程安全。
下面简单整理一下上面用到的类:
ResourceEntityResolver:继承自 EntityResolver ,通过 ResourceLoader 来解析实体的引用。
DelegatingEntityResolver:EntityResolver 的实现,分别代理了 dtd 的 BeansDtdResolver 和 xml schemas 的 PluggableSchemaResolver。
BeansDtdResolver : spring bean dtd 解析器。EntityResolver 的实现,用来从 classpath 或者 jar 文件加载 dtd。
PluggableSchemaResolver:使用ConcurrentHashMap存储 schema url和本地文件的位置,并将 schema url 解析到本地 classpath 资源,也就是我们自定义标签存放XSD文件的位置,后面我们说解析自定义标签会在说道这里。