文章目录
夯实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核心编程思想》