1. 程式人生 > >Spring如何載入XSD檔案

Spring如何載入XSD檔案

有時候你會發現過去一直啟動正常的系統,某天啟動時會報出形如下面的錯誤:

[plain] view plaincopyprint?
  1. org.xml.sax.SAXParseException: schema_reference.4: Failed to read schema document 'http://www.springframework.org/schema/beans/spring-beans-2.0.xsd', because 1) could not find the document; 2) the document could not be read; 3) the root element of the document is not <xsd:schema>.  
org.xml.sax.SAXParseException: schema_reference.4: Failed to read schema document 'http://www.springframework.org/schema/beans/spring-beans-2.0.xsd', because 1) could not find the document; 2) the document could not be read; 3) the root element of the document is not <xsd:schema>.


很顯然,spring xml配置檔案中指定的xsd檔案讀取不到了,原因多是因為斷網或spring的官網暫時無法連線導致的。 你可以通過在瀏覽器輸入xsd檔案的URL,如:http://www.springframework.org/schema/beans/spring-beans-2.0.xsd 進行確認。

關於這個問題,網上有兩種常見的解決方法,第一種簡單有效,但是工作量大,即:把所有spring配置檔案中url形式的xsd路徑轉換成指向本地xsd檔案的classpath形式的路徑,例如:classpath:org/springframework/beans/factory/xml/spring-beans-2.5.xsd ,再有一種方法就是在本機搭建web伺服器,按URL建立相應資料夾,放入對應xsd檔案,在本機hosts檔案中加入"127.0.0.1 www.springframework.org".實際上,這兩種方法都屬於“歪打正著”式的方法,直正弄明白這一問題還需要從spring的XSD檔案載入機制談起。

首先:你必須知道一點:spring在載入xsd檔案時總是先試圖在本地查詢xsd檔案(spring的jar包中已經包含了所有版本的xsd檔案),如果沒有找到,才會轉向去URL指定的路徑下載。這是非常合理的做法,並不像看上去的那樣,每次都是從站點下載的。事實上,假如你的所有配置是正確定的,你的工程完全可以在斷網的情況下啟動而不會報上面的錯誤。Spring載入xsd檔案的類是PluggableSchemaResolver,你可以檢視一下它的原始碼來驗證上述說法。另外,你可以在log4j.xml檔案中加入:

[html] view plaincopyprint?
  1. <loggername="org.springframework.beans.factory.xml">
  2. <levelvalue="all"/>
  3. </logger>
	<logger name="org.springframework.beans.factory.xml">
		<level value="all" />
	</logger>


通過日誌瞭解spring是何載入xsd檔案的。

接下來,問題就是為什麼spring在本地沒有找到需要的檔案,不得不轉向網站下載。關於這個問題,其實也非常簡單。在很多spring的jar包裡,在META-INF目錄下都有一個spring.schemas,這是一個property檔案,其內容類似於下面:

[plain] view plaincopyprint?
  1. http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans-2.0.xsd  
  2. http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans-2.5.xsd  
  3. http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd  
  4. ....  
http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans-2.0.xsd
http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans-2.5.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd
....


實際上,這個檔案就是spring關於xsd檔案在本地存放路徑的對映,spring就是通過這個檔案在本地(也就是spring的jar裡)查詢xsd檔案的。那麼,查詢不到的原因排除URL輸入有誤之外,可能就是宣告的xsd檔案版本在本地不存在。一般來說,新版本的spring jar包會將過去所有版本(應該是自2.0以後)的xsd打包,並在spring.schemas檔案中加入了對應項,出現問題的情況往往是宣告使用了一個高版本的xsd檔案,如3.0,但依賴的spring的jar包卻是2.5之前的版本,由於2.5版本自然不可能包含3.0的xsd檔案,此時就會導致spring去站點下載目標xsd檔案,如遇斷網或是目標站點不可用,上述問題就發生了。

但是,在實現開發中,出現上述錯誤的機率並不高,最常見的導致這一問題的原因其實與使用了一個名為“assembly”的maven打包外掛有關。很多專案需要將工程連同其所依賴的所有jar包打包成一個jar包,maven的assembly外掛就是用來完成這個任務的。但是由於工程往往依賴很多的jar包,而被依賴的jar又會依賴其他的jar包,這樣,當工程中依賴到不同的版本的spring時,在使用assembly進行打包時,只能將某一個版本jar包下的spring.schemas檔案放入最終打出的jar包裡,這就有可能遺漏了一些版本的xsd的本地對映,進而出現了文章開始提到的錯誤。如果你的專案是打成單一jar的,你可以通過檢查最終生成的jar裡的spring.schemas檔案來確認是不是這種情況。而關於這種情況,解決的方法一般是推薦使用另外一種打包外掛shade,它確實是一款比assembly更加優秀的工具,在對spring.schemas檔案處理上,shade能夠將所有jar裡的spring.schemas檔案進行合併,在最終生成的單一jar包裡,spring.schemas包含了所有出現過的版本的集合!

以上就是spring載入XSD檔案的機制和出現問題的原因分析。實際上,我們應該讓我們工程在啟動時總是載入本地的xsd檔案,而不是每次去站點下載,做到這一點就需要你結合上述提及的種種情況對你的工程進行一番檢查。

==========================

以前一直沒注意spring對xml的解析過程,
特別是xml檔案頭上的一堆xmlns:

這些名稱空間中是怎麼解析的,
大概可以分為下面這個步驟:
1. 解析XML, 找到所有的 名稱空間 如: http://www.springframework.org/schema/context
2. 在Classpath中查詢所有的 spring.handlers 並解析其中配置的 名稱空間 與 對應的處理類, 如:

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

3. 根據查詢到的處理Handler去解析配置檔案中相應的結點.

而名稱空間對應的xsd檔案, 則是在 spring.schemas 中指定的, 如:

http\://www.springframework.org/schema/aop/spring-aop-2.0.xsd=org/springframework/aop/config/spring-aop-2.0.xsd
http\://www.springframework.org/schema/aop/spring-aop-2.5.xsd=org/springframework/aop/config/spring-aop-2.5.xsd
http\://www.springframework.org/schema/aop/spring-aop-3.0.xsd=org/springframework/aop/config/spring-aop-3.0.xsd
http\://www.springframework.org/schema/aop/spring-aop.xsd=org/springframework/aop/config/spring-aop-3.0.xsd

====================

本文通過對 spring 原始碼的閱讀,解析 spring 如何校驗、解析 xml 格式的 bean 配置檔案,還會介紹如何擴充套件 spring,這也是我閱讀這部分程式碼的原因所在。

在使用 spring 的時候,最常用的配置 bean 的方式就是 xml 檔案,spring 本身提供了很多 xml namespace 的配置,如 jms、aop 等。我在使用這些 spring 內建的 namespace(其實也就用過 jms 這個namesapce) 的時候並沒有覺得有什麼疑問或奇怪的,但當我使用 activemq 以及瞭解 mule 的時候發現,它們的配置都是基於 spring 構建的,只是提供了它們自己的 namespace 而已。我很迷惑,它們這是怎麼實現的呢,難道這些框架本身也替換了 spring 實現的解析 spring bean xml 配置檔案的實現,想想也沒這必要,它們完全可以定義自己的 xml 配置檔案格式,然後自己實現解析,為什麼要嵌入到 spring 中呢?感覺很奇怪。帶著這個疑問,我就在查看了 spring 提供的 reference,在 spring reference 的 appendix B - Extensible XML authoring 中發現,原來可以通過 xml schema 來擴充套件 spring,並通過一些配置可以使 spring 識別並處理這些配置。這是疑問又出來了,spring 是怎麼實現的呢?於是決定讀讀原始碼,看看是如何實現的。為了節省時間,我從 XmlBeanFactory 開始,如果從 ApplicationContext 的實現開始的話,會遇到很多幹擾,因為 ApplicationContext 比 BeanFactory 提供了更多的更強大的功能。 spring 使用 XmlBeanDefinitionReader 來讀取並解析 xml 檔案,XmlBeanDefinitionReader 是 BeanDefinitionReader 介面的實現。BeanDefinitionReader 定義了 spring 讀取 bean 定義的一個介面,這個介面中有一些 loadBeanDefinitions 方法,從它們的方法簽名可知,spring 把讀取 bean 配置的來源抽象為 Resource 介面。BeanDefinitionReader 介面有兩個具體的實現,其中之一就是從 xml 檔案中讀取配置的 XmlBeanDefinitionReader,另一個則是從 java properties 檔案中讀取配置的 PropertiesBeanDefinitionReader。開發人員也可以提供自己的 BeanDefinitionReader 實現,根據自己的需要來讀取 spring bean 定義的配置。在 XmlBeanFactory 中建立了 XmlBeanDefinitionReader 的例項,並在 XmlBeanFactory 的構造方法中呼叫了 XmlBeanDefinitionReader 的 loadBeanDefinitions 方法,由 loadBeanDefinitions 方法負責載入 bean 配置並把 bean 配置註冊到 XmlBeanFactory 中。 loadBeanDefinitions 方法首先要通過 Resource 介面讀取 xml 配置檔案,並把它讀到一個 Document 物件中,用於解析,這個動作是由介面 DocumentLoader 的實現來完成的。spring 有一個預設實現 DefaultDocumentLoader。對於如何讀取一個 xml 檔案為 Document 物件,我想大部分都很熟悉:建立 DocumentBuilderFactory,由 DocumentBuilderFacoty 建立 DocumentBuidler,呼叫 DocumentBuilder 的 parse 方法把檔案或流解析為 Document。的確 spring 也是這樣做的,但有一點不要忘記,spring 需要使用 xml schema 來驗證 xml,spring 使用的 jaxp 1.2 中提供的 xml schema 驗證方式,並沒有使用 jaxp 1.3 中引入的 Schema 物件來驗證(jboss cache 也是使用的這種方式)。DefaultDocumentLoader 在建立了 DocumentBuilderFactory 物件後會判斷當前是否使用 xml schema 驗證,如果是則會在 DocumentBuiderFactory 上設定一個屬性,這個屬性名為 http://java.sun.com/xml/jaxp/properties/schemaLanguage,如果把這個屬性設定為 http://www.w3.org/2001/XMLSchema,jaxp 則會使用 xml schema 來驗證 xml 文件,使用這種驗證方式需要提供一個 EntityResolver 的實現,EntityResolver 的使用 DocumentBuilder 的 setEntityResolver 方法設定。spring 提供了 EntityResolver 的實現,這個實現也是擴充套件 spring 的關鍵所在,這個後面會提到,暫且略過。 在完成了 Resource 到 Document 的轉換後,下面就是從 Document 中解析出各個 bean 的配置了,為此 spring 又抽象了一個介面 BeanDefinitionDocumentReader,從它的名稱中可以一目瞭然這個介面負責從 Document 中讀取 bean 定義,這個介面中只定義了一個方法 registerBeanDefinitions。spring 也提供了一個預設實現 DefaultBeanDefinitionDocumentReader。DefaultBeanDefinitionDocumentReader 主要完成兩件事情,解析 <bean> 元素,為擴充套件 spring 的元素尋找合適的解析器,並把相應的元素交給解析器解析。第一個任務,解析 <bean> 元素,這個 spring 的核心功能及 IoC 或者是 DI,這由 spring 自己來處理,這個工作有一個專門的委託類來處理 BeanDefinitionParserDelegate,由它來解析 <bean> 元素,並把解析的結果註冊到 BeanDefinitionRegistry(XmlBeanFactory 實現了此介面) 中。那麼 spring 如何來區別 bean 元素以及其它擴充套件元素的,大家可能很自然地就能想到使用元素名啊,的確使用元素名可以處理,但這就會出現這樣的情況,程式設計師 A 擴充套件 spring 定一個元素名為 c 的元素,同樣程式設計師 B 擴充套件 spring 也定義了名為 c 的元素,此時就無法區分了。其實 spring 是通過 xml namespace 來區分的,同樣查詢擴充套件元素的解析器也是通過 xml namespace 來處理的。spring 從根元素開始,在解析每個元素的時候,都會先查詢元素的 namespace uri,如果元素的 namespace uri 為 http://www.springframework.org/schema/beans,則由 spring IoC 來解析處理,這些元素包括 beans、bean、import、alias,如果 namespace uri 不是 http://www.springframework.org/schema/beans,則會使用 NamespaceHandlerResolver 來解析出一個 NamespaceHandler,使用 NamespaceHandler 來解析處理這個元素。NamespaceHandlerResovler 和 NamespaceHandler 就是擴充套件 spring 的祕密所在。NamespaceHandlerResolver 是一個介面,spring 使用與 EntityResolver 相同的策略來實現,這個後面會提到。當這一步完成了 spring 也就完成了讀取解析 xml 配置。 上面僅僅是根據程式碼的執行路徑簡單地描述了 spring 解析 xml 配置的一個大體過程,至於更細節性的東西,感興趣的朋友不妨根據我上面的思路,詳細的讀一讀這一部分的原始碼。(我閱讀原始碼都是藉助於 ide 來完成的,這能提搞閱讀的速度,如果碰到一些是在不理解的程式碼還可以寫一些非常簡單的 demo 單步執行,看看程式碼到底是如何執行的) 下面介紹一下如何擴充套件 spring。不知大家有沒有注意到,在 spring 的原始碼目錄中有兩個很特殊的檔案:spring.schemas 和 spring.handlers,這兩個檔案以及 spring 中對 EntityResolver 和 NamespaceHandlerResolver 的實現 PluggableSchemaResolver 和 DefaultNamespaceHandlerResolver 是擴充套件 spring 的關鍵所在。其實 spring.schemas 和 spring.handlers 檔案是標準的 java properties 檔案。這兩個檔案都被大包到 spring jar 包中的 META-INF 目錄中,PluggableSchemaResolver 通過讀取 spring.schemas 檔案,根據 xml 檔案中實體的 system id 來解析這些實體,大家可以看一下 spring.schemas 檔案中的 key 就可以知道 system id 是什麼了(其實我也不知道 system id 和 public id 是啥,知道的朋友不妨在文後的回覆中給我留言,謝謝);而 DefaultNamespaceHandlerResolver 則是根據元素的 namespace uri 在 spring.handlers 檔案中查詢具體的 NamespaceHandler 的實現。 如上面所提到的,擴充套件 spring 需要完成以下幾個工作,定義一個 xml schema,並編寫相應的 spring.schemas 檔案,實現 NamespaceHandler 介面,根據需要還可能需要實現 BeanDefinitionParser 和 BeanDefinitionDecorator 等介面,更詳細的資訊可以參考 spring 的 reference 或者其他 spring 相關的文件,由於我想了解的事情已經瞭解了,就沒有繼續研究下去。 提到 擴充套件 spring 這裡簡單地說一下 activemq。說 activemq 通過擴充套件 spring 來實現本身的配置其實並不完全正確,事實上 activemq 並沒有擴充套件 spring,完全使用 spring 的 IoC 配置 activemq 也是可以實現的。activemq 本身的架構是基於 pojo 的,所以使用 spring 的 IoC 完成可以完成 activemq 的配置任務。但 spring 的 bean 配置看上去有點“專業化”,尤其是第一次看到 spring 配置的時候,會困惑與 property 元素(其實當了解了 spring 之後也是很好理解的)。開源社群裡不知道是哪位神人開發了 xbean 這樣一個框架。這個框架具體做什麼呢,它主要完成三件事情,第一根據原始碼中的一些特殊的 doclet 生成一個 xml schema,看看 activemq 的原始碼,大家可能會發現,很多類的 javadoc 中多了這樣一個 tag @org.apache.xbean.XBean 以及其它的一些 tag,xbean 會根據這些特殊的 tag 來生成一個 xml schema;xbean 完成的第二件事情就是它會生成擴充套件 spring 所需的一些配置;第三它重新實現了一些 spring 中的可替換元件,如它擴充套件了 XmlBeanDefinitionReader 實現了自己的 BeanDefinitionReader XBeanXmlDefinitionReader,實現了自己的 ApplicationContext ResourceXmlApplicationContext,如果使用了 xbean 就必須使用 xbean 實現的 ApplicationContext。xbean 提供的 BeanDefinitionReader 實現只是把一些定製的元素轉換成了 spring 中的 bean 元素,這樣使 spring 的配置更容易閱讀和理解。 本文簡單地介紹了 spring 解析 xml 配置以及擴充套件 spring 方面的一些內容,順帶還介紹了一下 xbean,感興趣的朋友可以詳細地讀一下這部分的原始碼。文中的解析並沒有上升到什麼架構或設計的高度,說實話自己還沒有達到這樣的高度,僅僅是對原始碼執行路徑的一個簡單描述,如果能使各位看官對 spring 的內部實現有一點的瞭解,或者使您對 spring 的內部實現感興趣並希望進一步瞭解,能達到這樣的效果我已經很知足了,別無他求。如果您有關於這方面的自己的想法不妨與大家一同分享。如果文中有言之有誤的地方,還望各位指正。