1. JDK的国际化组件
1.1. ResourceBundle
ResourceBundle是JDK提供的与国际化相关的组件,负责读取国际化配置文件,并将指定的键转成对应的值
我们先在类路径下创建一个i18n包,并在包中创建如下3个配置文件:
# message_en_US.properties
# 该配置文件在英文环境(en_US)下使用
123=One hundred and twenty-three
# message_zh_CN.properties
# 该配置文件在中文环境(zh_CN)下使用
123=一百二十三
# message.properties
# 默认的配置文件,如果找不到目标环境对应的配置文件,则使用该配置文件
123=123
测试程序如下:
public class ResourceBundleDemo {
public static void main(String[] args) {
// 读取指定的配置文件,配置文件中的数据将会存到ResourceBundle中
// 这里的两个参数分别是"i18n.message"和Locale.US,拼起来就对应了i18n包下的message_en_US.properties文件
ResourceBundle usBundle = ResourceBundle.getBundle("i18n.message", Locale.US);
// 打印"123"对应的值;这里打印:One hundred and twenty-three
System.out.println(usBundle.getObject("123"));
// 用相同的方式读取i18n包下的message_zh_CN.properties文件
ResourceBundle cnBundle = ResourceBundle.getBundle("i18n.message", Locale.CHINA);
// 这里打印:一百二十三
System.out.println(cnBundle.getObject("123"));
// 读取法语语境下的配置文件,此时显然读取不到
// 此时会获取到法语语境的备份语境(默认返回当前机器的语境,即中文语境)并读取相应的配置文件
// 因此,这里还是打印:一百二十三
ResourceBundle frBundle = ResourceBundle.getBundle("i18n.message", Locale.FRANCE);
System.out.println(frBundle.getObject("123"));
// 看到这里,我们发现,message.properties文件好像没用到啊,这个文件到底有什么用呢?
// 实际上,如果某个语境及其所有的备份语境都没有对应的配置文件,那么此时message.properties就会生效
// 如果我们把message_zh_CN.properties文件删除,则上面的3次打印结果中,后面两次就会变成"123"了
}
}
1.2. MessageFormat
MessageFormat也是JDK提供的组件,可以将字符串中的占位符替换成真实值;基本用法如下:
public class MessageFormatDemo {
/**
* 格式化消息模板,并打印格式化后的结果
*
* @param pattern 消息模板,可以有占位符;如"{0}"就代表取参数列表中第0个值
* @param args 参数列表
*/
private static void showFormatResult(String pattern, Object... args) {
System.out.println(MessageFormat.format(pattern, args));
}
public static void main(String[] args) {
// 这里会用"NightDW"字符串替换掉消息模板中的"{0}"子串
// 打印结果:Hello, my name is NightDW
showFormatResult("Hello, my name is {0}", "NightDW");
// 如果找不到占位符对应的参数,则该占位符会保持原样
// 打印结果:Hello, my name is {0}
showFormatResult("Hello, my name is {0}");
// 被单引号引用的文本,会被当作普通的字符串
// 这里打印:Hello, my name is {0};而不是:Hello, my name is 'NightDW'
showFormatResult("Hello, my name is '{0}'", "NightDW");
// 这里故意少了一个单引号,此时默认会往pattern的末尾添加一个单引号
// 这里打印:Hello, my name is {0}
showFormatResult("Hello, my name is '{0}", "NightDW");
// 如果想要打印单引号本身,则可以用连续两个单引号,前一个单引号就相当于转义符
// 这里打印:'
showFormatResult("''");
// 连续两个单引号的优先级大于单个单引号
// 这里打印:{'};而不是:{}
showFormatResult("'{''}'");
// 这里打印:{ }
showFormatResult("'{' '}'");
// 这里打印:{'NightDW{0}
showFormatResult("'{'''{0}'{0}'", "NightDW");
// 左右大括号的个数必须相同(不计算被当作纯文本的大括号),且不允许嵌套(存疑)
// 比如,"a {0} b"和"a '{' b"都是合法的,但"ab {0'}' de"和"{{0}}"是不合法的
// 我们还可以先对参数进行格式化,再将格式化后的字符串填充到对应的占位符中
// 比如下面这个例子,它会先将目标参数转成百分比的形式,再用转换后的字符串来替换占位符
// 这里底层是调用NumberFormat.getPercentInstance(getLocale())方法来获取格式化对象、并对目标参数进行格式化的
// 从getLocale()方法可以看出,MessageFormat底层也是有Locale对象的,它也可以根据语言环境将数据格式化为相应的字符串
// 这里打印:百分比:24%
showFormatResult("百分比:{0, number, percent}", 0.24);
// 同理,这里底层是调用DateFormat.getDateInstance(DateFormat.SHORT, getLocale())方法来获取格式化对象的
// 这里打印:日期:23-2-21
showFormatResult("日期:{0, date, short}", new Date());
// MessageFormat的比较常用的用法主要就是这些了,其它用法和注意事项可以自行查看文档
}
}
2. Spring的消息源接口
2.1. MessageSource
我们知道,AbstractApplicationContext底层维护了一个MessageSource组件;国际化消息的转换就是由该组件来完成的
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext {
/**
* 底层的MessageSource组件;该组件会在refresh阶段被初始化
*/
@Nullable
private MessageSource messageSource;
/**
* ApplicationContext接口本身继承了MessageSource接口
* 本类实现MessageSource接口方法的逻辑很简单,就是将其委派给底层的MessageSource组件
*/
@Override
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {
return getMessageSource().getMessage(code, args, defaultMessage, locale);
}
}
MessageSource接口的定义如下:
/**
* 解析消息的策略接口,支持消息的参数化(即允许有占位符)和国际化
*/
public interface MessageSource {
/**
* 尝试解析消息,如果没有找到,则返回默认消息
*
* @param code 消息的编码;相当于国际化配置文件中的key
* @param args 消息模板中的参数
* @param defaultMessage 默认消息
* @param locale 语言环境
*/
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
/**
* 尝试解析消息,如果解析不到,则抛异常
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
/**
* 尝试解析消息,如果解析不到,则抛异常
* 注意,MessageSourceResolvable接口中有getCodes()、getArguments()和getDefaultMessage()方法
* 本方法在解析MessageSourceResolvable对象时,会依次解析getCodes()方法返回的各个code,并返回第一个解析成功的结果
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
2.2. HierarchicalMessageSource
HierarchicalMessageSource是MessageSource的子接口,允许消息源有父级MessageSource
public interface HierarchicalMessageSource extends MessageSource {
/**
* 设置父级MessageSource;当解析不到消息时,会调用父级MessageSource继续解析
*/
void setParentMessageSource(@Nullable MessageSource parent);
/**
* 获取父级MessageSource
*/
@Nullable
MessageSource getParentMessageSource();
}
3. MessageSourceSupport
/**
* 本类一般作为MessageSource实现类的基类,主要完成了对默认消息的解析
*/
public abstract class MessageSourceSupport {
/**
* 本类会为默认的消息模板创建对应的MessageFormat
* 如果消息模板非法,导致MessageFormat创建失败,则INVALID_MESSAGE_FORMAT将会作为这个消息模板的MessageFormat
*/
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
/**
* 解析到消息模板后,在没有模板参数的情况下,是否依然使用MessageFormat对消息模板进行解析
* 一般来说,如果没有模板参数,那么此时是可以直接将这个消息模板返回的;这种情况下,该参数可以是false
* 但如果消息模板中包含MessageFormat的特有语法(比如消息模板为"'abc'",且不希望最终结果有单引号),则需要将该参数改为true
*/
private boolean alwaysUseMessageFormat = false;
/**
* MessageFormat缓存;key为消息模板,嵌套Map的key为语言环境,嵌套Map的value为对应的MessageFormat
* 需要注意的是,这里只会保存默认消息模板的MessageFormat,子类需要自己来维护通过code解析到的MessageFormat
*/
private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage = new HashMap<>();
// 省略setAlwaysUseMessageFormat()和isAlwaysUseMessageFormat()方法
/**
* 渲染默认消息;这里直接调用了本类的formatMessage()方法
*/
protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, Locale locale) {
return formatMessage(defaultMessage, args, locale);
}
/**
* 渲染消息;本方法接收到的消息始终都是默认消息,因此可以认为本方法是只用来解析默认消息的
*/
protected String formatMessage(String msg, @Nullable Object[] args, Locale locale) {
// 如果模板参数为空,且不强制使用MessageFormat,则直接返回消息本身
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
// 加锁,然后查询messageFormatsPerMessage缓存,获取到该消息在locale环境下的MessageFormat
// 这里直接对缓存进行加锁,而不是使用ConcurrentHashMap作为缓存,可能是因为解析默认消息的情况不多见
MessageFormat messageFormat = null;
synchronized (this.messageFormatsPerMessage) {
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
// 如果该消息有对应的嵌套Map,则继续查询嵌套Map;否则初始化嵌套Map
if (messageFormatsPerLocale != null) {
messageFormat = messageFormatsPerLocale.get(locale);
} else {
messageFormatsPerLocale = new HashMap<>();
this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);
}
// 如果没找到对应的MessageFormat,则尝试创建对应的MessageFormat,并将其放入缓存
if (messageFormat == null) {
try {
messageFormat = createMessageFormat(msg, locale);
} catch (IllegalArgumentException ex) {
// 执行到这里,说明MessageFormat创建失败
// 这种情况下,如果强制要求使用MessageFormat,那只能报错
if (isAlwaysUseMessageFormat()) {
throw ex;
}
// 否则,将INVALID_MESSAGE_FORMAT作为该消息模板对应的MessageFormat
messageFormat = INVALID_MESSAGE_FORMAT;
}
messageFormatsPerLocale.put(locale, messageFormat);
}
}
// 如果messageFormat是INVALID_MESSAGE_FORMAT,说明消息模板不合法,此时不管有没有模板参数,都只能返回消息模板本身
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
// 否则,对模板参数进行解析,然后通过messageFormat来对消息模板进行渲染
// 这里对messageFormat进行了加锁,可能因为format()方法或MessageFormat对象本身是线程不安全的
synchronized (messageFormat) {
return messageFormat.format(resolveArguments(args, locale));
}
}
/**
* 创建MessageFormat
*/
protected MessageFormat createMessageFormat(String msg, Locale locale) {
return new MessageFormat(msg, locale);
}
/**
* 解析模板参数;默认不对模板参数进行任何处理
*/
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
return (args != null ? args : new Object[0]);
}
}
4. AbstractMessageSource
4.1. 概述
/**
* 抽象的HierarchicalMessageSource实现类
* 本类实现了通用的消息处理逻辑,方便子类实现特定的策略
* 本类除了支持解析MessageSourceResolvable类型的消息之外,还允许该类型的数据作为模板参数
* 本类并没有为每个消息code缓存相应的消息,因此子类可以随时动态地更改code对应的消息
* 子类在缓存消息时,最好要能感知到消息的变更,以便实现消息的热部署
*/
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
/**
* 父级MessageSource
*/
@Nullable
private MessageSource parentMessageSource;
/**
* 用于保存一些和Locale无关的通用消息(这些消息适用于各种语言环境);key为消息的code,value为消息模板
*
* 注意,MessageSource对消息code的解析顺序为:
* 1. 先尝试自己解析
* 2. 如果解析不到,则查询commonMessages
* 3. 如果commonMessages中也没有,则在父级MessageSource中查询
* 4. 如果父级MessageSource中也没有,则返回默认消息(或抛异常)
*/
@Nullable
private Properties commonMessages;
/**
* 是否将消息的code作为该消息的默认消息,从而避免抛NoSuchMessageException
*
* 注意,如果要解析的MessageSourceResolvable有多个code,那么父级MessageSource的该字段不应该置为true,原因:
* 1. 假设子级MessageSource在尝试解析第一个code时,解析不到,且commonMessages中也没有对应的数据
* 2. 那么,此时会委托父级MessageSource来解析该code
* 3. 假设父级MessageSource同样无法解析该code,且commonMessages中也没有对应的数据
* 4. 这种情况下,如果父级MessageSource的该字段为true,那么父级MessageSource就会直接返回该code
* 5. 子级MessageSource发现解析到了数据,就会直接返回结果,而忽略了剩下的code
* 6. 而我们期望的是,子级MessageSource在尝试过所有的code且失败后,才返回默认值
*
* 当然,本类对上述这种情况是做了一定的处理的,解决方式是:
* 1. 定义一个getMessageInternal()方法
* 2. 该方法负责完成尝试自己解析、查询commonMessages、查询父级MessageSource这3步
* 3. 在查询父级MessageSource时,优先调用父级MessageSource的getMessageInternal()而不是getMessage()方法
* 4. 因此,getMessageInternal()方法不会返回默认值(除非父级MessageSource不是本类类型的,且有自己的默认值生成机制)
* 5. 这样的话,子级MessageSource在解析多个code时,可以通过getMessageInternal()方法来依次解析,避免提前获取到默认值
* 6. 尽管这样确实能解决问题,但一般来说,还是推荐只在开发环境下开启useCodeAsDefaultMessage,不要在生产中优先依赖这个功能
*/
private boolean useCodeAsDefaultMessage = false;
// 省略以上字段的set/get方法
}
4.2. getMessageInternal()
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
/**
* 在内部解析消息;该方法只做解析工作,即使解析不到,也不会返回默认值
*/
@Nullable
protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
if (code == null) {
return null;
}
// 如果没指定语言环境,则获取本地的语言环境
if (locale == null) {
locale = Locale.getDefault();
}
// argsToUse代表处理后的模板参数
Object[] argsToUse = args;
// 如果没有模板参数,且不强制使用MessageFormat,则调用resolveCodeWithoutArguments()方法来获取最终结果
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
// 否则,调用resolveCode()方法来获取MessageFormat,然后再渲染出最终结果
} else {
// 注意,模板参数中可能有MessageSourceResolvable类型的参数
// 因此,在将模板参数应用到消息模板中之前,需要先将MessageSourceResolvable类型的参数解析成具体的值
argsToUse = resolveArguments(args, locale);
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); // 这里的模板参数传的还是args,是不是应该改成argsToUse?
}
}
// 如果还是无法解析,则在父级MessageSource中解析
return getMessageFromParent(code, argsToUse, locale);
}
/**
* 重写父类的resolveArguments()方法;解析模板参数中的MessageSourceResolvable类型的参数
*/
@Override
protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) {
if (ObjectUtils.isEmpty(args)) {
return super.resolveArguments(args, locale);
}
List<Object> resolvedArgs = new ArrayList<>(args.length);
for (Object arg : args) {
if (arg instanceof MessageSourceResolvable) {
resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale));
} else {
resolvedArgs.add(arg);
}
}
return resolvedArgs.toArray();
}
/**
* 调用父级MessageSource来解析消息;优先调用父级MessageSource的getMessageInternal()方法,避免获取到默认值
*/
@Nullable
protected String getMessageFromParent(String code, @Nullable Object[] args, Locale locale) {
MessageSource parent = getParentMessageSource();
if (parent != null) {
if (parent instanceof AbstractMessageSource) {
return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale);
} else {
return parent.getMessage(code, args, null, locale);
}
}
return null;
}
}
4.3. 其它方法
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
@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;
}
// 如果用户没有指定默认消息模板,则根据code来生成默认消息;否则对用户指定的默认消息模板进行渲染
if (defaultMessage == null) {
return getDefaultMessage(code);
}
return renderDefaultMessage(defaultMessage, args, locale);
}
@Override
public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {
// 先尝试内部解析
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
// 再尝试通过code来获取默认消息;如果无法获取,则抛异常
String fallback = getDefaultMessage(code);
if (fallback != null) {
return fallback;
}
throw new NoSuchMessageException(code, locale);
}
@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
// 在内部依次解析MessageSourceResolvable中的code,如果解析成功,则直接返回对应的消息
String[] codes = resolvable.getCodes();
if (codes != null) {
for (String code : codes) {
String message = getMessageInternal(code, resolvable.getArguments(), locale);
if (message != null) {
return message;
}
}
}
// 执行到这里,说明所有code都解析失败了,此时根据MessageSourceResolvable来获取默认消息;获取不到则抛异常
String defaultMessage = getDefaultMessage(resolvable, locale);
if (defaultMessage != null) {
return defaultMessage;
}
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
}
/**
* 获取指定code对应的默认消息;如果允许code本身作为该code的默认消息,则返回该code,否则返回null
*/
@Nullable
protected String getDefaultMessage(String code) {
if (isUseCodeAsDefaultMessage()) {
return code;
}
return null;
}
/**
* 获取指定MessageSourceResolvable的默认消息模板,并对其进行渲染
*/
@Nullable
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
String defaultMessage = resolvable.getDefaultMessage();
String[] codes = resolvable.getCodes();
// 如果MessageSourceResolvable中的默认消息模板不为null
if (defaultMessage != null) {
// 如果MessageSourceResolvable不希望渲染该默认消息模板,则直接返回该模板
if (resolvable instanceof DefaultMessageSourceResolvable &&
!((DefaultMessageSourceResolvable) resolvable).shouldRenderDefaultMessage()) {
return defaultMessage;
}
// 如果默认消息模板和第一个code相同,说明MessageSourceResolvable希望将code本身作为默认消息,此时也不需要渲染
if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) {
return defaultMessage;
}
// 否则才对默认消息模板进行渲染
return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale);
}
// 如果没指定默认消息模板,则取第一个code对应的默认消息
return (!ObjectUtils.isEmpty(codes) ? getDefaultMessage(codes[0]) : null);
}
/**
* 将code解析成对应的消息
* 注意,本方法不需要使用MessageFormat来对消息进行格式化
* 但本方法默认还是通过resolveCode()方法获取了相应的MessageFormat,并调用其format()方法来进行格式化
* 为了提高效率,子类很有必要重写该方法,因为MessageFormat本身并不是很高效
*/
@Nullable
protected String resolveCodeWithoutArguments(String code, Locale locale) {
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(new Object[0]);
}
}
return null;
}
/**
* 抽象方法,负责将消息的code解析成相应的MessageFormat
* 这里返回MessageFormat而不是具体的字符串,是为了方便子类把MessageFormat缓存起来
*/
@Nullable
protected abstract MessageFormat resolveCode(String code, Locale locale);
}
5. AbstractResourceBasedMessageSource
/**
* 本类一般作为基于ResourceBundle的MessageSource的实现类的基类
* 本类主要提供了公共的配置方法,方便用户设置配置信息,也方便子类读取配置信息
*/
public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {
/**
* 国际化配置文件的basename
* 可以有多个basename,也就是说,允许用户指定多组国际化配置文件(可以是properties文件或xml文件)
* 假设basename为"a",那么a.properties、a.xml、a_zh_CN.properties、a_en_US.xml等文件都会被加载
*
* 注意,该集合是LinkedHashSet类型的,因此:
* 1. basename会按注册顺序进行排序(basename越靠前,它相应的配置文件的优先级越高)
* 2. 重复添加basename时,不会添加成功,也不会影响集合中的该basename的原有排序
*/
private final Set<String> basenameSet = new LinkedHashSet<>(4);
/**
* 文件的默认编码方式;如果为null,则采用系统默认的编码方式
* 该字段只对properties文件生效,对xml文件无效
*/
@Nullable
private String defaultEncoding;
/**
* 当找不到目标Locale对应的配置文件时,是否用本地Locale对应的配置文件作为备用配置文件
* 默认是true,也就是说,如果找不到法语配置文件,则会继续查找中文配置文件,再找不到才会查找basename.properties文件
* 但是,对于服务器应用来说,本地Locale并不一定能满足客户端的真实需求,因此这种情况下一般要将该字段置为false
*/
private boolean fallbackToSystemLocale = true;
/**
* 默认的Locale;该字段的优先级比fallbackToSystemLocale大
*/
@Nullable
private Locale defaultLocale;
/**
* 加载的配置文件的缓存时间;该字段和ResourceBundle.Control的getTimeToLive()方法的含义相似
* 1. 默认为-1,代表不过期(和ResourceBundle的默认行为相同)
* 2. 如果是0,则在获取消息前,会先检查文件的最后修改时间;不要在生产环境下使用!
* 3. 如果是正数,则每隔cacheMillis毫秒会刷新一次;每次刷新前,会先检查文件的最后修改时间,如果确实修改了,才重新加载
*/
private long cacheMillis = -1;
/**
* 获取fallbackToSystemLocale字段的值
* 注意该方法被废弃了,官方推荐直接调用getDefaultLocale()方法来获取默认的Locale
*/
@Deprecated
protected boolean isFallbackToSystemLocale() {
return this.fallbackToSystemLocale;
}
/**
* 获取默认的Locale,会根据defaultLocale和fallbackToSystemLocale这两个字段来返回结果
*/
@Nullable
protected Locale getDefaultLocale() {
if (this.defaultLocale != null) {
return this.defaultLocale;
}
if (this.fallbackToSystemLocale) {
return Locale.getDefault();
}
return null;
}
// 省略其它简单的set/get方法
}
6. ResourceBundleMessageSource
6.1. 成员变量
/**
* 基于ResourceBundler的MessageSource实现,底层通过MessageFormat组件来渲染消息模板
*
* 本类在读取ResourceBundle时,会使用getDefaultEncoding()的编码方式来进行编码
* 本类会同时缓存读取到的ResourceBundle和为每个消息生成的MessageFormat
* 本类提供的ResourceBundle缓存比JDK中ResourceBundle自带的缓存要快很多
* 本类重写了resolveCodeWithoutArguments()方法,以提高效率
*
* On the JDK 9+ module path where locally provided ResourceBundle.Control handles are not supported,
* this MessageSource always falls back to ResourceBundle.getBundle retrieval with the platform
* default encoding: UTF-8 with a ISO-8859-1 fallback on JDK 9+ (configurable through the
* "java.util.PropertyResourceBundle.encoding" system property). Note that loadBundle(Reader) and
* loadBundle(InputStream) won't be called in this case either, effectively ignoring override
* in subclasses. Consider implementing a JDK 9 java.util.spi.ResourceBundleProvider instead.
*/
public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware {
/**
* 加载配置文件时使用的类加载器
* 如果为null,则会采用beanClassLoader来加载配置文件
*/
@Nullable
private ClassLoader bundleClassLoader;
/**
* 用于接收该组件所在的BeanFactory所使用的类加载器
* 初始化为ClassUtils.getDefaultClassLoader(),这样,即使不在Spring容器中,该组件也能有一个备用类加载器
*/
@Nullable
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
/**
* 缓存加载到的ResourceBundle
* key为basename,嵌套Map的key为语言环境,嵌套Map的value为相应的ResourceBundle
*/
private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =
new ConcurrentHashMap<>();
/**
* 缓存已经生成的MessageFormat
* key为ResourceBundle,嵌套Map的key是消息code,嵌套Map的value是该code在不同语言环境下的MessageFormat
*/
private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats =
new ConcurrentHashMap<>();
/**
* 读取ResourceBundle时使用的控制组件,主要功能有:
* 1. 解析properties文件时,将编码设置为getDefaultEncoding()
* 2. 设置加载文件的缓存时间为getCacheMillis()
* 3. 设置备用的Locale为getDefaultLocale()
* 4. 当加载的文件过期时,清除掉cachedBundleMessageFormats中该ResourceBundle对应的缓存
*/
@Nullable
private volatile MessageSourceControl control = new MessageSourceControl();
}
6.2. 简单方法
public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware {
/**
* 构造方法;将properties文件的默认编码方式指定为ISO-8859-1
*/
public ResourceBundleMessageSource() {
setDefaultEncoding("ISO-8859-1");
}
// 省略bundleClassLoader和beanClassLoader字段的set方法
/**
* 获取加载配置文件时使用的类加载器
*/
@Nullable
protected ClassLoader getBundleClassLoader() {
return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader);
}
/**
* 重写AbstractMessageSource类的resolveCodeWithoutArguments()方法,避免使用MessageFormat,从而提高效率
*/
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
// 遍历注册的所有basename
for (String basename : basenames) {
// 拿到该basename在目标语言环境下对应的ResourceBundle
// 然后在ResourceBundle中查找该code对应的消息;如果能获取到,则直接返回该消息
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
String result = getStringOrNull(bundle, code);
if (result != null) {
return result;
}
}
}
// 执行到这里,说明所有的basename中都没有该code对应的消息
return null;
}
/**
* 将消息code解析成对应的MessageFormat;本方法的处理流程和上面的方法是一样的
*/
@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) {
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
/**
* 获取指定key对应的字符串类型的值;如果获取不到,则返回null
*/
@Nullable
protected String getStringOrNull(ResourceBundle bundle, String key) {
if (bundle.containsKey(key)) {
try {
return bundle.getString(key);
} catch (MissingResourceException ex) {
// 这里不把异常抛出是因为后续可能还需要在父级MessageSource中查找
}
}
return null;
}
/**
* 根据消息code获取对应的MessageFormat
*/
@Nullable
protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)
throws MissingResourceException {
// 先查缓存,如果有,则直接返回
Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);
Map<Locale, MessageFormat> localeMap = null;
if (codeMap != null) {
localeMap = codeMap.get(code);
if (localeMap != null) {
MessageFormat result = localeMap.get(locale);
if (result != null) {
return result;
}
}
}
// 否则,先拿到该code对应的消息模板
String msg = getStringOrNull(bundle, code);
// 如果确实有对应的消息模板,则创建对应的MessageFormat,将其放入缓存并返回
if (msg != null) {
if (codeMap == null) {
codeMap = this.cachedBundleMessageFormats.computeIfAbsent(bundle, b -> new ConcurrentHashMap<>());
}
if (localeMap == null) {
localeMap = codeMap.computeIfAbsent(code, c -> new ConcurrentHashMap<>());
}
MessageFormat result = createMessageFormat(msg, locale);
localeMap.put(locale, result);
return result;
}
// 如果没有对应的消息模板,则返回null
return null;
}
}
6.3. getResourceBundle()
public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware {
/**
* 根据basename和指定的Locale获取到对应的ResourceBundle
*/
@Nullable
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
// 如果设置了缓存过期时间,则直接调用doGetBundle()方法来处理
// doGetBundle()方法中,会自动检测ResourceBundle缓存是否过期,如果是,则重新加载
if (getCacheMillis() >= 0) {
return doGetBundle(basename, locale);
// 否则,说明缓存是永不过期的,此时走缓存查询
} else {
// 先查询缓存,如果能查到,则直接返回
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
}
// 否则,尝试加载ResourceBundle,并将其放入缓存中
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
return bundle;
} catch (MissingResourceException ex) {
// 如果没找到对应的ResourceBundle,则直接返回null
// 这里不把异常抛出是因为后续可能还需要在父级MessageSource中查找
return null;
}
}
}
/**
* 真正获取ResourceBundle
*/
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
ClassLoader classLoader = getBundleClassLoader();
Assert.state(classLoader != null, "No bundle ClassLoader set");
// 如果this.control不为null,则在加载时,让this.control也发挥作用
MessageSourceControl control = this.control;
if (control != null) {
try {
return ResourceBundle.getBundle(basename, locale, classLoader, control);
} catch (UnsupportedOperationException ex) {
// Probably in a Jigsaw environment on JDK 9+
// 如果出现异常,说明当前环境可能不支持ResourceBundle.Control,因此将this.control置为null
this.control = null;
// 如果还指定了默认的编码方式,则打印日志提示
String encoding = getDefaultEncoding();
if (encoding != null && logger.isInfoEnabled()) {
logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" +
encoding + "' but ResourceBundle.Control not supported in current system environment: " +
ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " +
"platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " +
"for participating in the platform default and therefore avoiding this log message.");
}
}
}
// 备用手段:加载ResourceBundle时不使用ResourceBundle.Control组件
return ResourceBundle.getBundle(basename, locale, classLoader);
}
// 其它和ResourceBundle.Control组件相关的方法和内部类先暂时省略
}
7. MessageSourceAutoConfiguration
// 表示当前是个配置类,且不需要为该配置类创建代理子类
@Configuration(proxyBeanMethods = false)
// 当Ioc容器本地没有名称为"messageSource"的Bean时,该配置类生效
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
// 该配置类的解析优先级为最大
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
// 该配置类生效的另一个条件:类路径(包括Jar包中的类路径)中必须包含${spring.messages.basename}.properties文件
@Conditional(ResourceBundleCondition.class)
// 启用@Bean方法上的@ConfigurationProperties注解
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
/**
* 将与国际化相关的配置封装成MessageSourceProperties对象并注册该Bean
* 也可以直接在本类上用@EnableConfigurationProperties(MessageSourceProperties.class)注解
*/
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
/**
* 根据国际化配置信息创建ResourceBundleMessageSource组件并注册该Bean
*/
@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;
}
/**
* 用于判断当前配置类是否需要生效
*/
protected static class ResourceBundleCondition extends SpringBootCondition {
private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
/**
* 获取匹配结果;匹配成功或失败都会生成相应的匹配结果
*/
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获取到用户配置的basename(允许配置多个,用逗号隔开);如果没有配置,则默认basename为"messages"
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
// 先查询缓存,如果有则直接返回,否则才真正获取匹配结果,并将其缓存起来
ConditionOutcome outcome = cache.get(basename);
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
/**
* 真正获取匹配结果
*/
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
// 根据逗号对参数basename进行切分,并依次处理获取到的真正的basename
for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
// 根据basename加载对应的properties文件,如果存在,则直接返回匹配成功的结果
for (Resource resource : getResources(context.getClassLoader(), name)) {
if (resource.exists()) {
return ConditionOutcome.match(message.found("bundle").items(resource));
}
}
}
// 否则,匹配失败,此时配置类不应该生效
return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}
/**
* 加载类路径(包括Jar包中的类路径)中的${name}.properties文件
*/
private Resource[] getResources(ClassLoader classLoader, String name) {
String target = name.replace('.', '/');
try {
return new PathMatchingResourcePatternResolver(classLoader)
.getResources("classpath*:" + target + ".properties");
} catch (Exception ex) {
return NO_RESOURCES;
}
}
}
}