本章会讨论 Spring 中国际化的接口和相关实现以及 Java 中对国际化的相关支持。虽然在 Spring 体系中,国际化属于比较边缘的技术,但是基于两点原因,我们也可以进行一些学习和了解
1.在 AbstractApplicationContext#refresh
应用上下文启动的过程中,initMessageSource()
来进行国际化的初始化,作为启动中重要的一环,不可避免需要学习和了解
2.后续章节中的很多内容和国际化结合的比较紧密,国际化主要提供一些文案的适配和支持
四个主要使用场景
核心接口
Spring 提供两个 out-of-the-box(开箱即用)的实现
主要概念
Spring 层次性接口回顾
Spring 层次性国际化接口
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
的方法,可以获取到双亲的相关信息,这里的双亲有可能是对象也有可能是对象的名称。
抽象实现 - java.util.ResourceBundle
列举实现 - java.util.ListResourceBundle(不常用)
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
Key - Value 设计
层次性
缓存设计
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)
核心接口
基本用法
消息格式模式
高级特性
示例
我们先用 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.
演示高级特性
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.
基于 ResourceBundle + MessageFomat 组合 MessageSource 实现
可重载 Properties + MessageFormat 组合 MessageSource 实现
MessageSource 内建 Bean 可能来源
messageSource
,类型为:MessageSource Bean源码分析
this.messageSource = dms;
将当前的 Application 和新建的 DelegatingMessageSource 实例对象做关联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 + "]");
}
}
}
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
spring.messages
相关内容封装成 MessageSourceProperties 对象@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;
}
...
上面 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 条件装配)
@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)
核心接口
层次性接口
主要技术
实现步骤大致分为 6 步
相关实现代码地址
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,实时修改保存之后,控制台打印结果会实时变化
控制台输出结果: