Spring 国际化使用场景
• 普通国际化文案
• Bean Validation 校验国际化文案
• Web 站点页面渲染
• Web MVC 错误消息提示
Spring 国际化接口
• 核心接口
- org.springframework.context.MessageSource
• 主要概念
- 文案模板编码(code)
- 文案模板参数(args)
- 区域(Locale)
层次性 MessageSource
• Spring 层次性接口回顾
- org.springframework.beans.factory.HierarchicalBeanFactory
- org.springframework.context.ApplicationContext
- org.springframework.beans.factory.config.BeanDefinition
• Spring 层次性国际化接口
- org.springframework.context.HierarchicalMessageSource
Java 国际化标准实现
• 核心接口
- 抽象实现 - java.util.ResourceBundle
- Properties 资源实现 - java.util.PropertyResourceBundle
- 例举实现 - java.util.ListResourceBundle
• ResourceBundle 核心特性
- Key-Value 设计
- 层次性设计
- 缓存设计
- 字符编码控制 - java.util.ResourceBundle.Control(@since 1.6)
- Control SPI 扩展 - java.util.spi.ResourceBundleControlProvider(@since 1.8)
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
/**
* {@link MessageFormat} 示例
*/
public class MessageFormatDemo {
public static void main(String[] args) {
int planet = 7;
String event = "a disturbance in the Force";
String messageFormatPattern = "At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.";
MessageFormat messageFormat = new MessageFormat(messageFormatPattern);
String result = messageFormat.format(new Object[]{planet, new Date(), event});
System.out.println(result);
// 重置 MessageFormatPattern
// applyPattern
messageFormatPattern = "This is a text : {0}, {1}, {2}";
messageFormat.applyPattern(messageFormatPattern);
result = messageFormat.format(new Object[]{"Hello,World", "666"});
System.out.println(result);
// 重置 Locale
messageFormat.setLocale(Locale.ENGLISH);
messageFormatPattern = "At {1,time,long} on {1,date,full}, there was {2} on planet {0,number,integer}.";
messageFormat.applyPattern(messageFormatPattern);
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);
}
}
MessageSource 开箱即用实现
• 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现
- org.springframework.context.support.ResourceBundleMessageSource
• 可重载 Properties + MessageFormat 组合 MessageSource 实现
- org.springframework.context.support.ReloadableResourceBundleMessageSource
MessageSource 內建依赖
• MessageSource 內建 Bean 可能来源
- 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean
- 默认內建实现 - DelegatingMessageSource
- 层次性查找 MessageSource 对象
课外资料
• Spring Boot 为什么要新建 MessageSource Bean?
- AbstractApplicationContext 的实现决定了 MessageSource 內建实现
- Spring Boot 通过外部化配置简化 MessageSource Bean 构建
- Spring Boot 基于 Bean Validation 校验非常普遍
/**
* Spring Boot 场景下自定义 {@link MessageSource} Bean
*/
@EnableAutoConfiguration
public class CustomizedMessageSourceBeanDemo { // @Configuration Class
/**
* 在 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 =
// Primary Configuration Class
new SpringApplicationBuilder(CustomizedMessageSourceBeanDemo.class)
.web(WebApplicationType.NONE)
.run(args);
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
if (beanFactory.containsBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME)) {
// 查找 MessageSource 的 BeanDefinition
System.out.println(beanFactory.getBeanDefinition(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME));
// 查找 MessageSource Bean
MessageSource messageSource = applicationContext.getBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
System.out.println(messageSource);
}
// 关闭应用上下文
applicationContext.close();
}
}
面试题
Spring 国际化接口有哪些?
答:
• 核心接口 - MessageSource
• 层次性接口 - org.springframework.context.HierarchicalMessageSource
Spring 有哪些 MessageSource 內建实现?
答:
• org.springframework.context.support.ResourceBundleMessageSource
• org.springframework.context.support.ReloadableResourceBundleMessageSource
• org.springframework.context.support.StaticMessageSource
• org.springframework.context.support.DelegatingMessageSource
如何实现配置自动更新 MessageSource?
答:
主要技术
• Java NIO 2:java.nio.file.WatchService
• Java Concurrency : java.util.concurrent.ExecutorService
• Spring:org.springframework.context.support.AbstractMessageSource
/**
* 动态(更新)资源 {@link MessageSource} 实现
* <p>
* 实现步骤:
* <p>
* 1. 定位资源位置( Properties 文件)
* 2. 初始化 Properties 对象
* 3. 实现 AbstractMessageSource#resolveCode 方法
* 4. 监听资源文件(Java NIO 2 WatchService)
* 5. 使用线程池处理文件变化
* 6. 重新装载 Properties 对象
*/
public class DynamicResourceMessageSource extends AbstractMessageSource implements ResourceLoaderAware {
private static final String resourceFileName = "msg.properties";
private static final String resourcePath = "/META-INF/" + resourceFileName;
private static final String ENCODING = "UTF-8";
private final Resource messagePropertiesResource;
private final Properties messageProperties;
private final ExecutorService executorService;
private ResourceLoader resourceLoader;
public DynamicResourceMessageSource() {
this.messagePropertiesResource = getMessagePropertiesResource();
this.messageProperties = loadMessageProperties();
this.executorService = Executors.newSingleThreadExecutor();
// 监听资源文件(Java NIO 2 WatchService)
onMessagePropertiesChanged();
}
private void onMessagePropertiesChanged() {
if (this.messagePropertiesResource.isFile()) { // 判断是否为文件
// 获取对应文件系统中的文件
try {
File messagePropertiesFile = this.messagePropertiesResource.getFile();
Path messagePropertiesFilePath = messagePropertiesFile.toPath();
// 获取当前 OS 文件系统
FileSystem fileSystem = FileSystems.getDefault();
// 新建 WatchService
WatchService watchService = fileSystem.newWatchService();
// 获取资源文件所在的目录
Path dirPath = messagePropertiesFilePath.getParent();
// 注册 WatchService 到 dirPath,并且关心修改事件
dirPath.register(watchService, ENTRY_MODIFY);
// 处理资源文件变化(异步)
processMessagePropertiesChanged(watchService);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 处理资源文件变化(异步)
*
* @param watchService
*/
private void processMessagePropertiesChanged(WatchService watchService) {
executorService.submit(() -> {
while (true) {
WatchKey watchKey = watchService.take(); // take 发生阻塞
// watchKey 是否有效
try {
if (watchKey.isValid()) {
for (WatchEvent event : watchKey.pollEvents()) {
Watchable watchable = watchKey.watchable();
// 目录路径(监听的注册目录)
Path dirPath = (Path) watchable;
// 事件所关联的对象即注册目录的子文件(或子目录)
// 事件发生源是相对路径
Path fileRelativePath = (Path) event.context();
if (resourceFileName.equals(fileRelativePath.getFileName().toString())) {
// 处理为绝对路径
Path filePath = dirPath.resolve(fileRelativePath);
File file = filePath.toFile();
Properties properties = loadMessageProperties(new FileReader(file));
synchronized (messageProperties) {
messageProperties.clear();
messageProperties.putAll(properties);
}
}
}
}
} finally {
if (watchKey != null) {
watchKey.reset(); // 重置 WatchKey
}
}
}
});
}
private Properties loadMessageProperties() {
EncodedResource encodedResource = new EncodedResource(this.messagePropertiesResource, ENCODING);
try {
return loadMessageProperties(encodedResource.getReader());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private Properties loadMessageProperties(Reader reader) {
Properties properties = new Properties();
try {
properties.load(reader);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return properties;
}
private Resource getMessagePropertiesResource() {
ResourceLoader resourceLoader = getResourceLoader();
Resource resource = resourceLoader.getResource(resourcePath);
return resource;
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
String messageFormatPattern = messageProperties.getProperty(code);
if (StringUtils.hasText(messageFormatPattern)) {
return new MessageFormat(messageFormatPattern, locale);
}
return null;
}
private ResourceLoader getResourceLoader() {
return this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader();
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public static void main(String[] args) throws InterruptedException {
DynamicResourceMessageSource messageSource = new DynamicResourceMessageSource();
for (int i = 0; i < 10000; i++) {
String message = messageSource.getMessage("name", new Object[]{}, Locale.getDefault());
System.out.println(message);
Thread.sleep(1000L);
}
}
}