概述 
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文件的位置,后面我们说解析自定义标签会在说道这里。