spring framework提供MessasgeSource来实现国际化。
MessageSource用法
准备properties文件,放在resources文件夹下面。这是默认语言和韩语的文件。
- i18n/message.properties
- i18n/message_ko.properties
文件里面的内容是key-value格式,使用{0}、{1}作为变量占位符:
argument.required=The {0} argument is required.
注册MessageSource Bean
spring提供了MessageSource的实现类ResourceBundleMessageSource。它从resources下查找多语言配置,并且会缓存结果。这是spring boot里MessageSource的初始化方法。
@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的几个属性,简单介绍下对应的效果。
- setBasenames:多语言文件的名字,多个名字用都好隔开。例如要使用上面两个文件,basenames要等于i18n/message。最终是基于basename+local+".properties"找到对应的多语言资源文件。
- setDefaultEncoding:设置多语言文件里编码格式。默认是ISO-8859-1,要改成UTF-8。
- setFallbackToSystemLocale:要获取默认Locale时,是不是要返回系统的Locale,也就是Locale.getDefault()。
- setCacheMillis:控制资源文件文件本地缓存的过期时间,默认-1表示不过期。
- setAlwaysUseMessageFormat:是否总是使用MessageFormat来解析多语言文本内容。
- setUseCodeAsDefaultMessage:code不存在时,是否直接返回code值。
使用MessageSource
MessageSource接口提供了三个API:
// 设置参数和默认值
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
// 设置参数
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
// 从多个code中返回第一个找到的值
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
这里都是获取单个code的方法,没有获取数组的API。我们可以参考application.properties里数组的配置方式,增强MessageSource功能。 数组的定义格式:
argument.required[0]=The {0} argument is required. argument.required[1]=The {0} argument is required.
public List<String> getMessageList(String code, Object[] args, Locale locale) {
List<String> result = new ArrayList<>();
int i = 0;
while(true) {
val listCode = String.format("%s[%d]", code, i++);
String message = getMessage(listCode, args, locale);
if (message == null) {
break;
}
result.add(message);
}
return result;
}
原理介绍
查找code的过程。
- 遍历basenames找到对应的资源文件
// 没有变量的情况
protected String resolveCodeWithoutArguments(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
String result = getStringOrNull(bundle, code);
if (result != null) {
return result;
}
}
}
return null;
}
// 有变量的情况,使用MessageFormat对变量进行替换
protected MessageFormat resolveCode(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
- 获得ResourceBundle
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
ClassLoader classLoader = getBundleClassLoader();
Assert.state(classLoader != null, "No bundle ClassLoader set");
MessageSourceControl control = this.control;
if (control != null) {
try {
return ResourceBundle.getBundle(basename, locale, classLoader, control);
}
catch (UnsupportedOperationException ex) {
// ...
}
}
// Fallback: plain getBundle lookup without Control handle
return ResourceBundle.getBundle(basename, locale, classLoader);
}
- 回调MessageSourceControl.newBundle()加载资源文件 ResourceBundle.getBundle(basename, locale, classLoader, control)方法最终回调control.newBundle()方法来查找资源文件。 MessageSourceControl重写了newBundle(),替换了java.properties的资源查找类型。
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
// Special handling of default encoding
if (format.equals("java.properties")) {
String bundleName = toBundleName(baseName, locale);
final String resourceName = toResourceName(bundleName, "properties");
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream inputStream = null;
if (reloadFlag) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
connection.setUseCaches(false);
inputStream = connection.getInputStream();
}
}
}
else {
inputStream = classLoader.getResourceAsStream(resourceName);
}
if (inputStream != null) {
String encoding = getDefaultEncoding();
if (encoding != null) {
try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) {
return loadBundle(bundleReader);
}
}
else {
try (InputStream bundleStream = inputStream) {
return loadBundle(bundleStream);
}
}
}
else {
return null;
}
}
else {
// 将 “java.class” 格式的处理委托给标准 Control
return super.newBundle(baseName, locale, format, loader, reload);
}
}
这里resourceName的格式是{baseName}_{locale}.properties。并且通过reload参数控制,是否重新加载资源文件。 最终用loadBundle(bundleReader)返回PropertyResourceBundle对象。 PropertyResourceBundle会在初始化的时候读取reader里的内容,存到一个Map<String, Object> lookup,后面就拿多语言code去Map里找。找的顺序是先从当前lookup里查,查不到再去parent.lookup里查。
public final Object getObject(String key) {
Object obj = handleGetObject(key);
if (obj == null) {
if (parent != null) {
obj = parent.getObject(key);
}
if (obj == null) {
throw new MissingResourceException("Can't find resource for bundle "
+this.getClass().getName()
+", key "+key,
this.getClass().getName(),
key);
}
}
return obj;
}
spring文档:Additional Capabilities of the ApplicationContext :: Spring Framework