MessageSource


 國際化MessageSource

  Spring中定義一個MessageSource接口,以用於支持信息的國際化和包含參數的信息的替換。MessageSource接口的定義如下:

public interface MessageSource {

    /**
     * 解析code對應的信息進行返回,如果對應的code不能被解析則返回默認信息defaultMessage。
     * @param 需要進行解析的code,對應資源文件中的一個屬性名
     * @param 需要用來替換code對應的信息中包含參數的內容,如:{0},{1,date},{2,time}
     * @param defaultMessage 當對應code對應的信息不存在時需要返回的默認值
     * @param locale 對應的Locale
     * @return
     */
    String getMessage(String code, Object[] args, String defaultMessage, Locale locale);

    /**
     * 解析code對應的信息進行返回,如果對應的code不能被解析則拋出異常NoSuchMessageException
     * @param code 需要進行解析的code,對應資源文件中的一個屬性名
     * @param args 需要用來替換code對應的信息中包含參數的內容,如:{0},{1,date},{2,time}
     * @param locale 對應的Locale
     * @return 
     * @throws NoSuchMessageException 如果對應的code不能被解析則拋出該異常
     */
    String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;

    /**
     * 通過傳遞的MessageSourceResolvable對應來解析對應的信息
     * @param resolvable 
     * @param locale 對應的Locale
     * @return 
     * @throws NoSuchMessageException 如不能解析則拋出該異常
     */
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}

  熟悉的ApplicationContext接口繼承MessageSource接口,所以所有的ApplicationContext實現類都實現了MessageSource接口,也就是可以通過ApplicationContext來調用MessageSource接口方法,以實現信息的國際化和替換信息中包含的參數。所有ApplicationContext實現類對MessageSource接口的實現,都是在AbstractApplicationContext中實現的,其對MessageSource接口實現的源碼如下:

@Override
public String getMessage(String code, Object args[], String defaultMessage, Locale locale) {
    return getMessageSource().getMessage(code, args, defaultMessage, locale);
}

@Override
public String getMessage(String code, Object args[], Locale locale) throws NoSuchMessageException {
    return getMessageSource().getMessage(code, args, locale);
}

@Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
    return getMessageSource().getMessage(resolvable, locale);
}

/**
 * Return the internal MessageSource used by the context.
 * @return the internal MessageSource (never {@code null})
 * @throws IllegalStateException if the context has not been initialized yet
 */
private MessageSource getMessageSource() throws IllegalStateException {
    if (this.messageSource == null) {
        throw new IllegalStateException("MessageSource not initialized - " +
                "call 'refresh' before accessing messages via the context: " + this);
    }
    return this.messageSource;
}

  從中我們可以看到AbstractApplicationContext對MessageSource的實現,都是自身所持有的MessageSource類型的messageSource對象來實現的。那么對應的messageSource又是如何初始化的呢?在其中定義了一個initMessageSource()來做對應的初始化工作,其源碼如下

/**
 * Initialize the MessageSource.
 * Use parent's if none defined in this context.
 */
protected void initMessageSource() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
        this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
        // Make MessageSource aware of parent MessageSource.
        if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
            HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
            if (hms.getParentMessageSource() == null) {
                // Only set parent context as parent MessageSource if no parent MessageSource
                // registered already.
                hms.setParentMessageSource(getInternalParentMessageSource());
            }
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Using MessageSource [" + this.messageSource + "]");
        }
    }
    else {
        // Use empty MessageSource to be able to accept getMessage calls.
        DelegatingMessageSource dms = new DelegatingMessageSource();
        dms.setParentMessageSource(getInternalParentMessageSource());
        this.messageSource = dms;
        beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
        if (logger.isDebugEnabled()) {
            logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME +
                    "': using default [" + this.messageSource + "]");
        }
    }
}

  從上述源碼中,可以看到如果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處理,否則就不處理。

  鑒於ApplicationContext實現類對MessageSource接口實現的這種機制,如果需要通過ApplicationContext來獲取國際化信息,那么只需要在對應的ApplicationContext中定義一個MessageSource類型的bean,並且指定對應的beanName為messageSource即可。Spring中對MessageSource提供了三個實現類,分別是ReloadableResourceBundleMessageSourceStaticMessageSourceResourceBundleMessageSource

ResourceBundleMessageSource
  ResourceBundleMessageSource是基於JDK ResourceBundle的MessageSource接口實現類。會將訪問過的ResourceBundle緩存起來,以便於下次直接從緩存中獲取進行使用。

示例
  如下在bean容器中定義一個messageSource和一個名為hello的bean,並指定messageSource的basename為message,即根資源文件應為類路徑下的message.properties,其它的是需要帶Locale后綴的,如中國大陸是message_zh_CN.properties。

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="message"/>
</bean>

<bean id="hello" class="com.app.Hello"/>

  其中類Hello的代碼如下所示,可以看到其通過實現ApplicationContextAware接口注入了當前的ApplicationContext對象,然后在其doSomething()方法中,我們通過注入的ApplicationContext對象以code為“appName”獲取從對應的國際化信息,這里在中文環境下默認就會從message_zh_CN.properties文件中獲取。

public class Hello implements ApplicationContextAware {

    private ApplicationContext context;
    
    public void doSomething() {
        String appName = context.getMessage("appName", null, null);
        System.out.println(appName);
    }
    
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.context = applicationContext;
    }

}

接下來我們在類路徑下分別建立message_zh_CN.properties文件和message.properties文件。其內容分別對應如下。

1、message.properties文件內容
  appName=test
2、message_zh_CN.properties文件內容
  appName=測試
接下來我們運行如下測試代碼,將看到控制台將輸出“測試”,因為在獲取對應信息時我們沒有指定Locale,所以默認會取當前的Locale,即zh_CN,所以對應的信息將從message_zh_CN.properties文件中獲取。

@ContextConfiguration({"classpath:/applicationContext.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class ApplicationContextTest {

    @Autowired
    private ApplicationContext context;
    
    @Test
    public void test() {
        Hello hello = context.getBean("hello", Hello.class);
        hello.doSomething();
    }
    
}

指定多個basename
  在上述示例中,我們通過setBasename()指定ResourceBundleMessageSource的一個對應資源文件的基名稱。但有時候我們可能會存在多種分類的資源文件,它們對應不同的基名稱,如view.properties、remind.properties等,對於這種情況我們就可以通過ResourceBundleMessageSource的setBasenames()方法來指定多個basename。

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
        <array>
            <value>remind</value>
            <value>view</value>
        </array>
    </property>
</bean>

defaultEncoding
  可以通過setDefaultEncoding()來指定將用來加載對應資源文件時使用的編碼,默認為空,表示將使用默認的編碼進行獲取。

fallbackToSystemLocale
  默認情況下,當指定Locale不存在某code對應的屬性時,默認將嘗試從系統對應的Locale中解析對應的code只有都不能解析時,才會使用基文件進行解析,如果還不能解析則將拋出異常。打個比方針對基名稱“message”我們有兩個屬性文件, message.properties和message_zh_CN.properties,其中前者定義了appName=test,且定義了hello=hello,而后者只定義了appName=測試,那么當我們通過如下代碼獲取對應code對應的信息時,輸出將如代碼中注釋所示,即輸出兩個“測試”,一個“hello”

System.out.println(this.messageSource.getMessage("appName", null, null));//測試
System.out.println(this.messageSource.getMessage("appName", null, Locale.ENGLISH));//測試
System.out.println(this.messageSource.getMessage("hello", null, Locale.CHINA));//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。

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="message"/>
    <property name="fallbackToSystemLocale" value="false"/>
</bean>

  當我們指定fallbackToSystemLocale為false后,再運行上述測試代碼時對應結果將如下。其中第一個跟第三個的輸出結果將不變,第二個將變為“test”,因為此時使用Locale.ENGLISH不能解析appName時將使用基文件message.properties來解析。

System.out.println(this.messageSource.getMessage("appName", null, null));//測試
System.out.println(this.messageSource.getMessage("appName", null, Locale.ENGLISH));//test
System.out.println(this.messageSource.getMessage("hello", null, Locale.CHINA));//hello

ReloadableResourceBundleMessageSource
  ReloadableResourceBundleMessageSource是以ResourceBundleMessageSource結尾的,但實際上它跟ResourceBundleMessageSource沒有什么直接的關系。                            ReloadableResourceBundleMessageSource也是對MessageSource的一種實現,其用法配置等和ResourceBundleMessageSource基本一致。所不同的是ReloadableResourceBundleMessageSource內部是使用PropertiesPersister來加載對應的文件,這包括properties文件和xml文件,然后使用java.util.Properties來保存對應的數據。

  另外,ReloadableResourceBundleMessageSource允許我們指定非類路徑下的文件作為對應的資源文件,而ResourceBundleMessageSource是限制我們只能將對應的資源文件放置在類路徑下的。在指定basename時,還可以使用Spring支持的資源文件的前綴,如classpath等。

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="classpath:message"/>
    <property name="fallbackToSystemLocale" value="false"/>
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

cacheSeconds
  ReloadableResourceBundleMessageSource也是支持緩存對應的資源文件的,默認的緩存時間為永久,即獲取一次資源文件后就將其緩存起來,以后再也不重新去獲取該文件。這個可以通過setCacheSeconds()方法來指定對應的緩存時間,單位為秒。前面我們已經說過ResourceBundleMessageSource也是會緩存對應的資源文件的,而且其也可以通過setCacheSeconds()方法指定對應的緩存時間,但是即使指定了也不會生效,其不會對緩存過的文件重新加載。

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="classpath:message"/>
    <property name="cacheSeconds" value="1200"/>
</bean>

concurrentRefresh
  ReloadableResourceBundleMessageSource使用concurrentRefresh屬性來控制是否允許並發刷新,默認為true。其表示當線程A正在刷新緩存的資源文件F時,線程B也准備刷新緩存的資源文件F,那么線程A將繼續執行刷新緩存的資源文件F的動作,而線程B將直接獲取到原來緩存的資源文件F,當然這里也可以是取的線程A剛剛刷新的那個資源文件F。如果我們設定concurrentRefresh為false,那么先獲取到對應鎖的線程A將先刷新緩存中的資源文件F,然后在其釋放對應的鎖后,線程B將獲取到對應的鎖並再一次刷新緩存中的資源文件F。

注入MessageSource
  除了直接使用ApplicationContext對象來獲取對應code的國際化信息外,我們還可以給對應的bean直接注入一個MessageSource對象以直接通過對應的MessageSource對象來獲取對應code的國際化信息。給bean注入MessageSource主要有兩種方式,一種是直接注入,一種是間接的通過實現MessageSourceAware接口進行注入

直接注入
  直接注入就可以跟普通bean注入一樣進行注入,可以使用注解標注進行注入,也可以使用XML配置進行注入。以下是一個使用XML方式通過set方法進行注入的示例。

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="message"/>
</bean>

<bean id="hello" class="com.app.Hello">
    <property name="messageSource" ref="messageSource"/>
</bean>

對應Hello的定義如下

public class Hello {

    private MessageSource messageSource;
    
    public void doSomething() {
        String appName = this.messageSource.getMessage("appName", null, null);
        System.out.println(appName);
    }
    
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

}

實現MessageSourceAware接口
  當一個bean實現了MessageSourceAware接口時,ApplicationContext在實例化對應的bean后,會將自己作為MessageSource回調MessageSourceAware實現類的setMessageSource()方法以實現MessageSource的注入。如下代碼中Hello類就實現了MessageSourceAware接口。

public class Hello implements MessageSourceAware {

    private MessageSource messageSource;
    
    public void doSomething() {
        String appName = this.messageSource.getMessage("appName", null, null);
        System.out.println(appName);
    }
    
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

}

  

 

參考

  原文:https://blog.csdn.net/elim168/article/details/77891450 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM