文章目錄
夯實Spring系列|第十七章:Spring 國際化(i18n)
前言
本章會討論 Spring 中國際化的接口和相關實現以及 Java 中對國際化的相關支持。雖然在 Spring 體系中,國際化屬於比較邊緣的技術,但是基於兩點原因,我們也可以進行一些學習和了解
1.在 AbstractApplicationContext#refresh
應用上下文啓動的過程中,initMessageSource()
來進行國際化的初始化,作爲啓動中重要的一環,不可避免需要學習和了解
2.後續章節中的很多內容和國際化結合的比較緊密,國際化主要提供一些文案的適配和支持
1.項目環境
- jdk 1.8
- spring 5.2.2.RELEASE
- github 地址:https://github.com/huajiexiewenfeng/thinking-in-spring
- 本章模塊:i18n
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 對象。
如果我們需要看到 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";
}
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,實時修改保存之後,控制檯打印結果會實時變化
控制檯輸出結果:
10.參考
- 極客時間-小馬哥《小馬哥講Spring核心編程思想》