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 handlereturn 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 encodingif (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” 格式的处理委托给标准 Controlreturn 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