夯實Spring系列|第十七章:Spring 國際化(i18n)

夯實Spring系列|第十七章:Spring 國際化(i18n)

前言

本章會討論 Spring 中國際化的接口和相關實現以及 Java 中對國際化的相關支持。雖然在 Spring 體系中,國際化屬於比較邊緣的技術,但是基於兩點原因,我們也可以進行一些學習和了解

1.在 AbstractApplicationContext#refresh 應用上下文啓動的過程中,initMessageSource() 來進行國際化的初始化,作爲啓動中重要的一環,不可避免需要學習和了解

2.後續章節中的很多內容和國際化結合的比較緊密,國際化主要提供一些文案的適配和支持

1.項目環境

2.Spring 國際化使用場景

四個主要使用場景

  • 普通國際化文案
  • Bean Validation 效驗國際化文案
    • 這一部分在下一章進行討論
  • Web 站點頁面渲染
  • Web MVC 錯誤消息提示

3.Spring 國際化接口

核心接口

  • org.springframework.context.MessageSource

Spring 提供兩個 out-of-the-box(開箱即用)的實現

  • org.springframework.context.support.ResourceBundleMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource

主要概念

  • 文案模板編碼(code)
  • 文案模板參數(args)
  • 區域(Locale)
    • java.util.Locale

3.層次性 MessageSource

Spring 層次性接口回顧

  • org.springframework.beans.factory.HierarchicalBeanFactory
  • org.springframework.context.ApplicationContext
  • org.springframework.beans.factory.config.BeanDefinition

Spring 層次性國際化接口

  • org.springframework.context.HierarchicalMessageSource
public interface HierarchicalMessageSource extends MessageSource {

	/**
	 * Set the parent that will be used to try to resolve messages
	 * that this object can't resolve.
	 * @param parent the parent MessageSource that will be used to
	 * resolve messages that this object can't resolve.
	 * May be {@code null}, in which case no further resolution is possible.
	 */
	void setParentMessageSource(@Nullable MessageSource parent);

	/**
	 * Return the parent of this MessageSource, or {@code null} if none.
	 */
	@Nullable
	MessageSource getParentMessageSource();

}

層次性接口都有一個共同的特點,一般會有一個 getParent 的方法,可以獲取到雙親的相關信息,這裏的雙親有可能是對象也有可能是對象的名稱。

4.Java 國際化標準實現

4.1 核心接口

  • 抽象實現 - java.util.ResourceBundle

    • 列舉實現 - java.util.ListResourceBundle(不常用)

      • sun.security.util.AuthResources_zh_CN
      • 通過硬編碼的方式將文案相關的信息維護成一個二維數組
      public class AuthResources_zh_CN extends ListResourceBundle {
          private static final Object[][] contents = new Object[][]{
          {"invalid.null.input.value", "無效的空輸入: {0}"}, 
          {"NTDomainPrincipal.name", "NTDomainPrincipal: {0}"}, 
          {"NTNumericCredential.name", "NTNumericCredential: {0}"}, 
          {"Invalid.NTSid.value", "無效的 NTSid 值"}, {"NTSid.name", "NTSid: {0}"}, 
          ...
      
          public AuthResources_zh_CN() {
          }
      
          public Object[][] getContents() {
              return contents;
          }
      }
      
    • Properties 資源實現 - java.util.PropertyResourceBundle

      • 使用 Properties 文件中的文案信息進行相應轉換

4.2 ResourceBundle 核心特性

  • Key - Value 設計

  • 層次性

    • java.util.ResourceBundle#setParent
    • java.util.ResourceBundle#getObject
      • 查找對象的時候,會先從 parent 中進行查找
    • java.util.ResourceBundle#containsKey
  • 緩存設計

    • java.util.ResourceBundle.CacheKey
        private static final ConcurrentMap<CacheKey, BundleReference> cacheList
            = new ConcurrentHashMap<>(INITIAL_CACHE_SIZE);
    
  • 字段編碼控制 - java.util.ResourceBundle.Control(@since 1.6)

  • Control SPI 擴展- java.util.spi.ResourceBundleContorlProvider(@since 1.8)

5.Java 文本格式化

核心接口

  • java.text.MessageFormat

基本用法

  • 設置消息格式模式 - new MessageFormat(…)
  • 格式化 - format(new Object[]{…})

消息格式模式

  • 格式元素:{ArgumentIndex,(FormatType),(FormatStyle)}
  • FormatType:消息格式類型,可選項,每種類型在 number、date、time 和 choice 類型選其一
  • FormatStyle:消息格式風格,可選項,包括:short、medium、long、full、integer、currency、percent

高級特性

  • 重置消息格式模式
  • 重置 java.util.Locale
  • 重置 java.text.Format

示例

我們先用 java doc 中提供的示例

    private static void javaDocDemo() {
        int planet = 7;
        String event = "a disturbance in the Force";

        String result = MessageFormat.format(
                "At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.",
                planet, new Date(), event);

        System.out.println(result);
    }

執行結果:

At 上午09時52分06秒 on 2020年6月12日 星期五, there was a disturbance in the Force on planet 7.

演示高級特性

  • 重置消息格式模式
  • 重置 java.util.Locale
  • 重置 java.text.Format
    public static void main(String[] args) {
        javaDocDemo();

        // 重置 MessageFormatPatten
        MessageFormat messageFormat = new MessageFormat("This is a text : {0}");
        messageFormat.applyPattern("This is a new text : {0}");
        String result = messageFormat.format(new Object[]{"hello,world"});
        System.out.println(result);

        // 重置 Locale
        messageFormat.setLocale(Locale.ENGLISH);
        messageFormat.applyPattern("At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.");
        int planet = 7;
        String event = "a disturbance in the Force";
        result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);

        // 重置 Format
        // 根據參數索引來設置 Pattern
        messageFormat.setFormat(1,new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"));
        result = messageFormat.format(new Object[]{planet, new Date(), event});
        System.out.println(result);
    }

執行結果:

At 上午09時52分06秒 on 2020年6月12日 星期五, there was a disturbance in the Force on planet 7.
This is a new text : hello,world
At 9:52:06 AM CST on Friday, June 12, 2020, there was a disturbance in the Force on planet 7.
At 9:52:06 AM CST on 2020-06-12 09:52:06, there was a disturbance in the Force on planet 7.

6.MessageSource 開箱即用實現

基於 ResourceBundle + MessageFomat 組合 MessageSource 實現

  • org.springframework.context.support.ResourceBundleMessageSource

可重載 Properties + MessageFormat 組合 MessageSource 實現

  • org.springframework.context.support.ReloadableResourceBundleMessageSource

7.MessageSource 內建實現

MessageSource 內建 Bean 可能來源

  • 預註冊 Bean 名稱爲:messageSource,類型爲:MessageSource Bean
  • 默認內建實現- DelegatingMessageSource
    • 層次性查找 MessageSource 對象

源碼分析

  • org.springframework.context.support.AbstractApplicationContext#initMessageSource
    • beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME) 判斷當前上下文中是否包含這個 bean
    • 如果不包含,第一次進來顯然是不包含的
      • beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); 註冊一個 MessageSource 的實例對象放到 IOC 容器中,這個對象是通過 new DelegatingMessageSource() 創建,通過 jdk 的註釋可以看到這是一個空實現
      • 然後通過 this.messageSource = dms; 將當前的 Application 和新建的 DelegatingMessageSource 實例對象做關聯
    • 如果包含,表示我們已經通過別的方式將 MessageSource 對象註冊到當前上下文中,那麼通過依賴查找的方式獲取這個 bean 對象,並與當前的 Application 做關聯
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.isTraceEnabled()) {
         logger.trace("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.isTraceEnabled()) {
         logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
      }
   }
}

8.Spring Boot 中應用

8.1 Spring Boot 爲什麼要新建 MessageSource Bean?

  • AbstractApplicationContext 的實現決定 MessageSource 內建實現
    • 從第 7 小結中的分析可以看到,Spring上下文啓動過程中,初始化 MessageSource 相關的代碼,會先判斷我們是否有自己的實現,以我們的實現爲主,Spring Boot 可以使用這個特性新建自己的 MessageSource Bean
  • Spring Boot 通過外部化配置簡化 MessageSource Bean 構建
  • Spring Boot 基於 Bean Validation 效驗非常普遍(主要原因)
    • MessageSource 可以提供相關的文案

8.2 MessageSource 自動裝配

org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration

  • 先通過外部化配置的方式將 Properties 文件中前綴爲 spring.messages 相關內容封裝成 MessageSourceProperties 對象
  • 再通過 @Bean 的方式注入 MessageSource Bean 對象
    • 這裏最好的方式是 @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME) 使用常量的名稱限定,保證這個 Bean 一定滿足 AbstractApplicationContext#initMessageSource 中的判斷條件
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

	private static final Resource[] NO_RESOURCES = {};

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

	@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(properties.getBasename())) {
			messageSource.setBasenames(StringUtils
					.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}
    ...

8.3 示例

8.3.1 條件裝配分析

上面 MessageSource 自動裝配的實現中有兩個條件裝配

  • @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
  • @Conditional(ResourceBundleCondition.class)

第一個條件裝配的意思是如果當前上下文中不存在 Bean 的 name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME 時,纔會進行自動裝配;換言之,如果我們註冊一個名稱爲 AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME Bean對象到當前上下文中,Spring Boot 中 MessageSource 的自動裝配就會失效

第二個條件裝配的意思是,需要在 resources 目錄下面建立一個 messages.properties 的文件(細節就不展開了可以參考 走向自動裝配|第三章-Spring Boot 條件裝配

  • MessageSourceAutoConfiguration.ResourceBundleCondition#getMatchOutcome

8.3.2 測試代碼

@EnableAutoConfiguration
public class CustomizedMessageSourceBeanDemo {

    /**
     * 在 Spring Boot 場景中,Primary Configuration Sources(Classes) 高於 *AutoConfiguration
     * @return
     */
    @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)
    public MessageSource messageSource(){
        return new ReloadableResourceBundleMessageSource();
    }
    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(CustomizedMessageSourceBeanDemo.class, args);
        ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        if (beanFactory.containsLocalBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)) {
            MessageSource messageSource = applicationContext.getBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
            System.out.println(messageSource);
        }
        applicationContext.close();
    }
}

執行結果:

可以看到當前上下文中的 MessageSource 對象是我們自己定義的 ReloadableResourceBundleMessageSource 對象。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IBQWoUAk-1591933251662)(G:\workspace\csdn\learn-document\spring-framework\csdn\image-20200612110657733.png)]
如果我們需要看到 Spring 中的默認實現 DelegatingMessageSource

可以註釋掉 @EnableAutoConfiguration 和 @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)

執行結果:

DelegatingMessageSource#toString 源碼如下:所以輸出的是 Empty MessageSource

	@Override
	public String toString() {
		return this.parentMessageSource != null ? this.parentMessageSource.toString() : "Empty MessageSource";
	}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6D80AmPP-1591933251665)(G:\workspace\csdn\learn-document\spring-framework\csdn\image-20200612111221081.png)]
Spring Boot 中的默認實現 ResourceBundleMessageSource

可以註釋掉 @Bean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)

執行結果:
在這裏插入圖片描述

9.面試題

9.1 Spring 國際化接口有哪些?

核心接口

  • org.springframework.context.MessageSource

層次性接口

  • org.springframework.context.HierarchicalMessageSource

9.2 Spring 有哪些 MessageSource 內建實現?

  • org.springframework.context.support.ResourceBundleMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource
  • org.springframework.context.support.StaticMessageSource
  • org.springframework.context.support.DelegatingMessageSource

9.3 如何實現配置自動更新 MessageSource ?

主要技術

  • Java NIO 2 : java.nio.file.WatchService
  • Java Concurrency : java.util.concurrent.ExecutorService
  • Spring : org.springframework.context.support.AbstractMessageSource

實現步驟大致分爲 6 步

  • 1.定位資源位置(properties 文件)
  • 2.初始化 Properties 對象
  • 3.實現 AbstractMessageSource#resolveCode
  • 4.監聽資源文件(Java NIO 2 WatchService)
  • 5.線程池處理文件變化
  • 6.重新裝載 Properties 對象

相關實現代碼地址

github相關實現類 DynamicResourceMessageSource.java

調用代碼

    public static void main(String[] args) throws InterruptedException {
        DynamicResourceMessageSource source = new DynamicResourceMessageSource();
        for (int i = 0; i < 10000; i++) {
            System.out.println(source.getMessage("name", new Object[]{}, Locale.getDefault()));
            Thread.sleep(1000L);
        }
    }

啓動之後,找到 target/classes/META-INF 下面的 msg.properties 文件修改,保存文件 ctrl+s,實時修改保存之後,控制檯打印結果會實時變化
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5R8eElMZ-1591933251667)(G:\workspace\csdn\learn-document\spring-framework\csdn\image-20200612112947026.png)]
控制檯輸出結果:
在這裏插入圖片描述

10.參考

  • 極客時間-小馬哥《小馬哥講Spring核心編程思想》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章