摘要:本文結合《Spring源碼深度解析》來分析Spring 5.0.6版本的源代碼。若有描述錯誤之處,歡迎指正。
這一篇開始進行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的配置文件,代碼如下:
<?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的實現類,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; }