Spring源码之:国际化

575 阅读9分钟

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

HierarchicalMessageSourceMessageSource的子接口,允许消息源有父级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;
            }
        }
    }
}