【源码】关于 Spring 国际化 MessageSource
前言
容器类顶层接口 ApplicationContext 同时也是一个 MessageSource,MessageSource 便是所谓的 国际化 接口,它的 getMessage
方法允许我们根据不同的 Locale 返回不同的信息结果
本章节集合部分源码解读其原理,并给出 SpringBoot 的实现和使用示例
版本
Spring Framework:5.2.x
MessageSource
public interface MessageSource {
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
顶层接口,提供了 getMessage
的三个重载方法:
String code
:待解析的字符串Object[] args
:待解析字符串的参数,可以基于 MessageFormat 解析带有 占位符 的字符串String defaultMessage
:读取不到 国际化 信息时依据此默认值进行处理MessageSourceResolvable resolvable
,对上述几个参数的封装,默认实现类 DefaultMessageSourceResolvableLocale locale
:国际化定位,如Locale.CHINA
代表中国
HierarchicalMessageSource
public interface HierarchicalMessageSource extends MessageSource {
void setParentMessageSource(@Nullable MessageSource parent);
@Nullable
MessageSource getParentMessageSource();
}
拓展了 MessageSource 的层级关系
DelegatingMessageSource
Spring 容器启动时,如果容器中不存在对应的 Bean 实例,则容器中默认创建的 MessageSource 实例类型为 DelegatingMessageSource
具体源码参考 AbstractApplicationContext#initMessageSource 方法
该类的主要作用是试图将 国际化 处理委托给父级 MessageSource,否则降级处理:处理成默认字符串或者抛出异常
public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
@Nullable
private MessageSource parentMessageSource;
@Override
public void setParentMessageSource(@Nullable MessageSource parent) {
this.parentMessageSource = parent;
}
@Override
@Nullable
public MessageSource getParentMessageSource() {
return this.parentMessageSource;
}
@Override
@Nullable
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
/**
* 由 parentMessageSource 处理
*/
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);
}
// 依据 defaultMessage 继续处理
else if (defaultMessage != null) {
return renderDefaultMessage(defaultMessage, args, locale);
}
else {
return null;
}
}
@Override
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, locale);
}
// 抛出异常
else {
throw new NoSuchMessageException(code, locale);
}
}
// ...
}
AbstractMessageSource
核心抽象类,实现 配置化解析国际化信息 的关键类,我们从 getMessage
方法的实现入手
getMessage
@Override
public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
/**
* code 的国际化处理
*/
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
/**
* 如果处理结果为 null,且不提供 defaultMessage
* 则根据属性 useCodeAsDefaultMessage
* 决定原样返回,还是抛出异常,默认抛出异常
*/
if (defaultMessage == null) {
return getDefaultMessage(code);
}
// 依据 defaultMessage 处理
return renderDefaultMessage(defaultMessage, args, locale);
}
getMessageInternal
方法处理信息解析useCodeAsDefaultMessage
属性决定解析无结果的信息是原样返回还是抛出异常,默认抛出异常renderDefaultMessage
即依据defaultMessage
解析信息解析
重点关注 getMessageInternal
方法
getMessageInternal
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)) {
/**
* 无参信息的处理
* 本质是直接映射对应 Locale 的配置文件
* 方法由子类复写
*/
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
}
else {
argsToUse = resolveArguments(args, locale);
/**
* 有参信息的处理
* 本质是基于 MessageFormat 进行处理,
* 同时缓存对应的 MessageFormat
* 方法由子类复写
*/
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(argsToUse);
}
}
}
// 基于 commonMessages 处理信息
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
// 最后,尝试交给父级处理
return getMessageFromParent(code, argsToUse, locale);
}
- 对于无参信息交给
resolveCodeWithoutArguments
方法处理,其本质就是读取对应 Locale 的配置文件,具体由子类实现 - 对于有参信息交给
resolveCode
方法处理,其本质是读取对应 Locale 的配置文件后,基于 MessageFormat 和对应的参数进行处理,同时进行缓存,具体由子类实现 - 之后,会尝试基于
commonMessages
进行处理 - 最后,尝试交给父级处理
重点关注子类对 resolveCodeWithoutArguments
和 resolveCode
方法的实现
AbstractResourceBasedMessageSource
public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {
// 配置文件路径名
private final Set<String> basenameSet = new LinkedHashSet<>(4);
// 字符集
@Nullable
private String defaultEncoding;
private boolean fallbackToSystemLocale = true;
// 默认 Locale
@Nullable
private Locale defaultLocale;
// 缓存时间,默认永久
private long cacheMillis = -1;
// ... getter & setter
}
在 AbstractMessageSource 的基础上,拓展了配置文件相关属性的管理
ResourceBundleMessageSource
核心实现类,也是 SpringBoot 提供的默认实现,我们重点关注其对上述提到的 resolveCodeWithoutArguments
和 resolveCode
方法的实现
resolveCodeWithoutArguments
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
/**
* 根据不同的 Locale 加载对应的 ResourceBundle
* 因为没有参数,就相当于单纯的配置文件映射
* 基于 basenameSet 加载对应路径的配置文件
* 基于 ResourceBundle 类来管理配置文件
*/
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;
}
- 基于
basenameSet
属性和 Locale 定位对应的配置文件 - 配置文件由 ResourceBundle 类管理
- 直接读取对应的
key
即可 - 因此使用 ResourceBundleMessageSource 时我们需要定义具体的
basenameSet
,SpringBoot 给出的默认实现中,basenameSet
默认为messages
,可以由spring.messages.basename
属性自定义,后文会给出示例
resolveCode
@Override
@Nullable
protected MessageFormat resolveCode(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
/**
* 由对应的 bundle code locale 创建对应的
* MessageFormat 缓存后返回
* 缓存数据结构 Map<String, Map<Locale, MessageFormat>>
*/
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
- 同样的,基于
basenameSet
属性和 Locale 定位对应的配置文件 - 配置文件由 ResourceBundle 类管理
- 基于
bundle
code
locale
创建对应的 MessageFormat 并缓存起来,其数据结构为Map<String, Map<Locale, MessageFormat>>
- 默认创建的 MessageFormat 为
new MessageFormat(msg, locale)
,这基本就够用了,如果需要,也可以自行复写MessageSourceSupport#createMessageFormat
方法
示例
此处我们给出 SpringBoot 中使用 ResourceBundleMessageSource 的示例,首先,SpringBoot 默认 自动装配 的 MessageSource 即就是 ResourceBundleMessageSource
详情参考类 MessageSourceAutoConfiguration
因此,我们仅需要简单地配置即可使用:
application.yaml
spring:
messages:
basename: i18n/messages
encoding: UTF-8
配置文件路径设置为 i18n/messages
,编码为 UTF-8
i18n/messages
类路径下创建配置文件 messages.properties
,同时再创建一个 messages_zh_CN.properties
用于匹配示例中的 LocaleCHINA
,如图:
demo
@SpringBootApplication
public class MsApplication {
@Autowired
MessageSource messageSource;
public static void main(String[] args) {
SpringApplication.run(MsApplication.class, args);
}
@Component
class test implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 无参信息处理
System.out.println(
messageSource.getMessage("hello", null, Locale.CHINA));
// 有参信息处理
System.out.println(
messageSource.getMessage("time", new Object[]{ new Date() }, Locale.CHINA));
}
}
ReloadableResourceBundleMessageSource
还有另一个实现类 ReloadableResourceBundleMessageSource,同时支持对 xml
配置文件的解析,且它结合 basename
属性和 Locale 对配置文件的路径进行匹配
在 SpringBoot 中,它的使用与 ResourceBundleMessageSource 基本相似,但是如果要使用 ReloadableResourceBundleMessageSource,需要单独创建对应的 Bean实例,如下示例
示例
@SpringBootApplication
public class MsApplication {
@Autowired
MessageSource messageSource;
public static void main(String[] args) {
SpringApplication.run(MsApplication.class, args);
}
@Component
class test implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// 无参解析
System.out.println(
messageSource.getMessage("hello", null, Locale.CHINA));
// 有参解析
System.out.println(
messageSource.getMessage("time", new Object[]{ new Date() }, Locale.CHINA));
}
}
/**
* 此处避免循环依赖,创建在静态内部类中
* 当然,一般情况单独由配置类管理
*/
@Configuration
static class MessageSourceCofiguration {
/**
* 声明 MessageSource 的 bean 实例为
* ReloadableResourceBundleMessageSource
*/
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource
= new ReloadableResourceBundleMessageSource();
reloadableResourceBundleMessageSource.setBasename("i18n/messages");
reloadableResourceBundleMessageSource.setDefaultEncoding("UTF-8");
reloadableResourceBundleMessageSource.setCacheMillis(2000);
return reloadableResourceBundleMessageSource;
}
}
}
其他代码、配置文件如出一辙,无需改变
总结
以上,就是对 Spring 国际化的介绍,主要介绍了 MessageSource 体系下的各类,以及对 SpringBoot 中的使用方式进行了具体的示范