Spring中定義了一個MessageSource接口,以用于支援資訊的國際化和包含參數的資訊的替換。MessageSource接口的定義如下,對應的方法說明已經在方法上注釋了。
我們熟悉的ApplicationContext接口繼承了MessageSource接口,是以我們所有的ApplicationContext實作類都實作了MessageSource接口,也就是我們我們可以通過ApplicationContext來調用MessageSource接口方法以實作資訊的國際化和替換資訊中包含的參數。所有ApplicationContext實作類對MessageSource接口的實作都是在AbstractApplicationContext中實作的,其對MessageSource接口實作的源碼如下:
從中我們可以看到AbstractApplicationContext對MessageSource的實作都是自身所持有的MessageSource類型的messageSource對象來實作的。那麼對應的messageSource又是如何初始化的呢?在其中定義了一個initMessageSource()來做對應的初始化工作,其源碼如下。
從上述源碼中我們可以看到如果bean容器中存在一個名為messageSource(MESSAGE_SOURCE_BEAN_NAME常量對應的值為messageSource)的bean,則取該bean作為messageSource,如果對應的messageSource是一個HierarchicalMessageSource,則會在父容器存在的情況下取父容器對應的messageSource作為目前messageSource的parentMessageSource。如果目前bean容器中不存在beanName為messageSource的bean,則會生成一個DelegatingMessageSource來作為目前的MessageSource。DelegatingMessageSource基本算是對MessageSource的一個空的實作,在對應父容器的messageSource存在時就使用父容器的messageSource處理,否則就不處理,具體可以參考Spring的API文檔或檢視DelegatingMessageSource的源碼。
鑒于ApplicationContext實作類對MessageSource接口實作的這種機制,如果我們需要通過ApplicationContext來擷取國際化資訊,那麼我們隻需要在對應的ApplicationContext中定義一個MessageSource類型的bean,并且指定對應的beanName為messageSource即可。Spring中對MessageSource提供了三個實作類,分别是ReloadableResourceBundleMessageSource、StaticMessageSource和ResourceBundleMessageSource。
ResourceBundleMessageSource是基于JDK ResourceBundle的MessageSource接口實作類。它會将通路過的ResourceBundle緩存起來,以便于下次直接從緩存中擷取進行使用。
如下我們在bean容器中定義了一個messageSource和一個名為hello的bean,并指定了messageSource的basename為message,即根資源檔案應為類路徑下的message.properties,其它的都是需要帶Locale字尾的,如中國大陸是message_zh_CN.properties。
其中類Hello的代碼如下所示,我們可以看到其通過實作ApplicationContextAware接口注入了目前的ApplicationContext對象,然後在其doSomething()方法中我們通過注入的ApplicationContext對象以code為“appName”擷取從對應的國際化資訊,這裡在中文環境下預設就會從message_zh_CN.properties檔案中擷取。
接下來我們在類路徑下分别建立message_zh_CN.properties檔案和message.properties檔案。其内容分别對應如下。
1、message.properties檔案内容
2、message_zh_CN.properties檔案内容
接下來我們運作如下測試代碼,将看到控制台将輸出“測試”,因為在擷取對應資訊時我們沒有指定Locale,是以預設會取目前的Locale,即zh_CN,是以對應的資訊将從message_zh_CN.properties檔案中擷取。
在上述示例中我們通過setBasename()指定了ResourceBundleMessageSource的一個對應資源檔案的基名稱。但有時候我們可能會存在多種分類的資源檔案,它們對應不同的基名稱,如view.properties、remind.properties等,對于這種情況我們就可以通過ResourceBundleMessageSource的setBasenames()方法來指定多個basename。
可以通過setDefaultEncoding()來指定将用來加載對應資源檔案時使用的編碼,預設為空,表示将使用預設的編碼進行擷取。
預設情況下,當指定Locale不存在某code對應的屬性時,預設将嘗試從系統對應的Locale中解析對應的code,隻有都不能解析時才會使用基檔案進行解析,如果還不能解析則将抛出異常。打個比方針對基名稱“message”我們有兩個屬性檔案, message.properties和message_zh_CN.properties,其中前者定義了appName=test,且定義了hello=hello,而後者隻定義了appName=測試,那麼當我們通過如下代碼擷取對應code對應的資訊時,輸出将如代碼中注釋所示,即輸出兩個“測試”,一個“hello”。
第一個輸出因為我們沒有指定Locale,則預設會使用目前系統的Locale,即中文環境,此時将從message_zh_CN.properties中擷取對應code對應的資訊,是以輸出為“測試”。第二個是因為我們沒有定義Locale.ENGLISH對應的本地資源檔案,預設将使用目前系統的Locale,即Locale.CHINA,是以輸出為“測試”。第三個是因為Locale.CHINA對應的資源檔案message_zh_CN.properties檔案中不存在對應code的屬性,是以将從基檔案message.properties中擷取,即輸出為“hello”。
前面已經提到了當指定Locale不能解析指定的code時,預設将使用系統目前的Locale來解析對應的code,這是通過ResourceBundleMessageSource的fallbackToSystemLocale屬性來定義的,預設為true,我們可以通過對應的set方法來設定其值為false,以使在指定的Locale不能解析指定的code時将使用基檔案來解析對應的code。
當我們指定了fallbackToSystemLocale為false後,再運作上述測試代碼時對應結果将如下。其中第一個跟第三個的輸出結果将不變,第二個将變為“test”,因為此時使用Locale.ENGLISH不能解析appName時将使用基檔案message.properties來解析。
關于ResourceBundleMessageSource的更多資訊請參考Spring API文檔,或檢視對應的源碼。
ReloadableResourceBundleMessageSource是以ResourceBundleMessageSource結尾的,但實際上它跟ResourceBundleMessageSource沒有什麼直接的關系。ReloadableResourceBundleMessageSource也是對MessageSource的一種實作,其用法配置等和ResourceBundleMessageSource基本一緻。所不同的是ReloadableResourceBundleMessageSource内部是使用PropertiesPersister來加載對應的檔案,這包括properties檔案和xml檔案,然後使用java.util.Properties來儲存對應的資料。
另外,ReloadableResourceBundleMessageSource允許我們指定非類路徑下的檔案作為對應的資源檔案,而ResourceBundleMessageSource是限制了我們隻能将對應的資源檔案放置在類路徑下的。在指定basename時,我們還可以使用Spring支援的資源檔案的字首,如classpath等。
ReloadableResourceBundleMessageSource也是支援緩存對應的資源檔案的,預設的緩存時間為永久,即擷取了一次資源檔案後就将其緩存起來,以後再也不重新去擷取該檔案。這個可以通過setCacheSeconds()方法來指定對應的緩存時間,機關為秒。前面我們已經說過ResourceBundleMessageSource也是會緩存對應的資源檔案的,而且其也可以通過setCacheSeconds()方法指定對應的緩存時間,但是即使指定了也不會生效,其不會對緩存過的檔案重新加載。
ReloadableResourceBundleMessageSource使用concurrentRefresh屬性來控制是否允許并發重新整理,預設為true。其表示當線程A正在重新整理緩存的資源檔案F時,線程B也準備重新整理緩存的資源檔案F,那麼線程A将繼續執行重新整理緩存的資源檔案F的動作,而線程B将直接擷取到原來緩存的資源檔案F,當然這裡也可以是取的線程A剛剛重新整理的那個資源檔案F。如果我們設定concurrentRefresh為false,那麼先擷取到對應鎖的線程A将先重新整理緩存中的資源檔案F,然後在其釋放對應的鎖後,線程B将擷取到對應的鎖并再一次重新整理緩存中的資源檔案F。
除了直接使用ApplicationContext對象來擷取對應code的國際化資訊外,我們還可以給對應的bean直接注入一個MessageSource對象以直接通過對應的MessageSource對象來擷取對應code的國際化資訊。給bean注入MessageSource主要有兩種方式,一種是直接注入,一種是間接的通過實作MessageSourceAware接口進行注入。
直接注入就可以跟普通bean注入一樣進行注入,可以使用注解标注進行注入,也可以使用XML配置進行注入。以下是一個使用XML方式通過set方法進行注入的示例。
對應Hello的定義如下。
當一個bean實作了MessageSourceAware接口時,ApplicationContext在執行個體化對應的bean後會将自己作為MessageSource回調MessageSourceAware實作類的setMessageSource()方法以實作MessageSource的注入。如下代碼中Hello類就實作了MessageSourceAware接口。
(注:本文是基于Spring4.1.0所寫)