Spring国际化详解,Spring国家化实例及源码详解
文章目录
- 一、概述
- 1、使用场景
- 二、Java 国际化标准实现
- 1、Java文本格式化
- 三、Spring 国际化接口
- 1、层次性MessageSource
- 2、MessageSource 开箱即用实现
- ResourceBundleMessageSource
- ReloadableResourceBundleMessageSource
- 3、MessageSource 內建依赖
- 源码分析
- 4、Spring Boot 为什么要新建 MessageSource Bean?
- 5、实现配置自动更新 MessageSource
- 参考资料
一、概述
Spring国际化相关的,似乎平时接触的也很少,也很少用到这部分的技术。
但是做架构、框架封装的时候,又不得不考虑。
1、使用场景
(1)普通国际化文案
就是我们网页上显示的和我们输入的一些支持多语言的字体或文案。
(2)Bean Validation 校验国际化文案
Springboot场景用的比较多的、默认加载的Bean Validation(用于Bean校验的JSR标准)。
(3)Web 站点页面渲染
比如说一个web页面,不同的国家ip或者其他信息,会收到不同的页面渲染,文字、布局会发生变化。
(4)Web MVC 错误消息提示
国际化场景下访问一些链接、API,会有一些文字性的描述,可能会存在国际化的提示。
二、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)
1、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
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* {@link MessageFormat} 示例
* @see MessageFormat
*/
public class MessageFormatDemo {
public static void main(String[] args) {
int planet = 7;
String event = "a disturbance in the Force";
// {0} 、 {1} 等代表占位符
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);
// 也可以使用静态方法,其实也是用的new方式创建的
String formatResult = MessageFormat.format(messageFormatPattern, new Object[]{planet, new Date(), event});
// 重置 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,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);
}
}
MessageFormat类的doc注释中有着大量的实例,这里就不一一列举了。
三、Spring 国际化接口
核心接口:org.springframework.context.MessageSource
主要概念:文案模板编码(code)、文案模板参数(args)、区域(Locale)
Locale类主要是靠语言、国家、语言变种的方式来定位。
MessageSource接口有两个开箱即用的实现(与java国际化类ResourceBundle密不可分):
org.springframework.context.support.ResourceBundleMessageSource, org.springframework.context.support.ReloadableResourceBundleMessageSource
1、层次性MessageSource
Spring 层次性接口:
- org.springframework.beans.factory.HierarchicalBeanFactory
- org.springframework.context.ApplicationContext
- org.springframework.beans.factory.config.BeanDefinition
有关Spring层次性接口更多详情请看(3、层次性依赖查找(接口 - HierarchicalBeanFactory)):
spring依赖查找、依赖注入深入学习及源码分析
Spring 层次性国际化接口:
- org.springframework.context.HierarchicalMessageSource
HierarchicalMessageSource接口继承了MessageSource接口,增加了对parent的操作。
2、MessageSource 开箱即用实现
ResourceBundleMessageSource
基于 ResourceBundle + MessageFormat 组合 MessageSource 实现:
org.springframework.context.support.ResourceBundleMessageSource
关键方法源码分析:
// org.springframework.context.support.AbstractMessageSource#getMessage(java.lang.String, java.lang.Object[], java.lang.String, java.util.Locale)
@Override
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
if (defaultMessage == null) {
return getDefaultMessage(code);
}
return renderDefaultMessage(defaultMessage, args, locale);
}
// org.springframework.context.support.AbstractMessageSource#getMessageInternal
@Nullable
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
if (code == null) {
return null;
}
if (locale == null) {
locale = Locale.getDefault();
}
Object[] argsToUse = args;
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
// Optimized resolution: no arguments to apply,
// therefore no MessageFormat needs to be involved.
// Note that the default implementation still uses MessageFormat;
// this can be overridden in specific subclasses.
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
}
else {
// Resolve arguments eagerly, for the case where the message
// is defined in a parent MessageSource but resolvable arguments
// are defined in the child MessageSource.
argsToUse = resolveArguments(args, locale);
// 通过code关联模板,然后通过java的MessageFormat 进行翻译
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
// Check locale-independent common messages for the given message code.
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
// Not found -> check parent, if any.
return getMessageFromParent(code, argsToUse, locale);
}
此时resolveCode方法就是在ResourceBundleMessageSource实现的:
// org.springframework.context.support.ResourceBundleMessageSource#resolveCode
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
Set<String> basenames = getBasenameSet();// 使用LinkedHashSet缓存
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
//使用ConcurrentHashMap缓存,只读MessageFormat,不可以set
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
ReloadableResourceBundleMessageSource
可重载 Properties + MessageFormat 组合 MessageSource 实现:
org.springframework.context.support.ReloadableResourceBundleMessageSource
ReloadableResourceBundleMessageSource的getMessage同样是父类AbstractMessageSource处理的,与ResourceBundleMessageSource处理逻辑相同,只是在resolveCode方法中有所不同。
// org.springframework.context.support.ReloadableResourceBundleMessageSource#resolveCode
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
}
return null;
}
ReloadableResourceBundleMessageSource加载配置文件有个重新加载的功能,判断文件的上次修改时间是否有变化:
(这个实现其实没多大用处,1、配置文件通常都在classPath下,通常不会变;2、lastModified不一定全部存在,有可能全部返回-1)
// org.springframework.context.support.ReloadableResourceBundleMessageSource#refreshProperties
protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) {
long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis());
Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
if (!resource.exists()) {
resource = this.resourceLoader.getResource(filename + XML_SUFFIX);
}
if (resource.exists()) {
long fileTimestamp = -1;
if (getCacheMillis() >= 0) {
// Last-modified timestamp of file will just be read if caching with timeout.
try {
fileTimestamp = resource.lastModified();
if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
if (logger.isDebugEnabled()) {
logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified");
}
propHolder.setRefreshTimestamp(refreshTimestamp);
return propHolder;
}
}
catch (IOException ex) {
// Probably a class path resource: cache it forever.
if (logger.isDebugEnabled()) {
logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", ex);
}
fileTimestamp = -1;
}
}
try {
Properties props = loadProperties(resource, filename);
propHolder = new PropertiesHolder(props, fileTimestamp);
}
catch (IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex);
}
// Empty holder representing "not valid".
propHolder = new PropertiesHolder();
}
}
else {
// Resource does not exist.
if (logger.isDebugEnabled()) {
logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML");
}
// Empty holder representing "not found".
propHolder = new PropertiesHolder();
}
propHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, propHolder);
return propHolder;
}
3、MessageSource 內建依赖
MessageSource 內建 Bean 可能来源:
- 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean(Springboot启动时已经注册了)
- 默认內建实现 - DelegatingMessageSource:层次性查找 MessageSource 对象
源码分析
IOC容器启动时,会调用initMessageSource方法初始化。
关于IOC容器启动流程请移步:
spring系列-注解驱动原理及源码-spring容器创建流程
// org.springframework.context.support.AbstractApplicationContext#initMessageSource
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { // 只在当前beanFactory找,并不在parent找
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 { // 如果找不到MessageSource,新建一个
// Use empty MessageSource to be able to accept getMessage calls.
DelegatingMessageSource dms = new DelegatingMessageSource();
dms.setParentMessageSource(getInternalParentMessageSource()); // paremt
this.messageSource = dms;
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); // 注册singleton Bean
if (logger.isTraceEnabled()) {
logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
}
}
}
4、Spring Boot 为什么要新建 MessageSource Bean?
- AbstractApplicationContext 的实现决定了 MessageSource 內建实现。
- Spring Boot 通过外部化配置简化 MessageSource Bean 构建。
- Spring Boot 基于 Bean Validation 校验非常普遍
Springboot通过MessageSourceAutoConfiguration自动化装配MessageSource,通过外部化配置ResourceBundleMessageSource的方式创建MessageSource。
我们也可以自定义MessageSource来替换默认自动装配的MessageSource:
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
/**
* Spring Boot 场景下自定义 {@link MessageSource} Bean
*
* @see MessageSource
* @see MessageSourceAutoConfiguration
* @see ReloadableResourceBundleMessageSource
*/
@EnableAutoConfiguration
public class CustomizedMessageSourceBeanDemo { // @Configuration Class
/**
* 在 Spring Boot 场景中,Primary Configuration Sources(Classes) 高于 *AutoConfiguration
*/
@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();
}
}
注意:需要在resources目录下创建文件messages.properties。
5、实现配置自动更新 MessageSource
import org.springframework.context.MessageSource;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.StringUtils;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.*;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
/**
* 动态(更新)资源 {@link MessageSource} 实现
* <p>
* 实现步骤:
* <p>
* 1. 定位资源位置( Properties 文件)
* 2. 初始化 Properties 对象
* 3. 实现 AbstractMessageSource#resolveCode 方法
* 4. 监听资源文件(Java NIO 2 WatchService)
* 5. 使用线程池处理文件变化
* 6. 重新装载 Properties 对象
*
* @see MessageSource
* @see AbstractMessageSource
* @since
*/
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);
}
}
}
参考资料
极客时间-《小马哥讲 Spring 核心编程思想》