• SpringBoot实战:国际化组件MessageSource的执行逻辑与源码


    SpringBoot 实战:国际化组件 MessageSource 的执行逻辑与源码

    你好,我是看山。

    前文介绍了 SpringBoot 中的国际化组件MessageSource的使用,本章我们一起看下ResourceBundleMessageSourceReloadableResourceBundleMessageSource的执行逻辑。SpringBoot 的 MessageSource 组件有很多抽象化,源码看起来比较分散,所以本文会通过流程图的方式进行讲解。

    配置文件

    配置文件是基础,会影响执行逻辑,我们先来看下配置项:

    • basename:加载资源的文件名,可以多个资源名称,通过逗号隔开,默认是“messages”;
    • encoding:加载文件的字符集,默认是 UTF-8,这个不多说;
    • cacheDuration:文件加载到内存后缓存时间,默认单位是秒。如果没有设置,只会加载一次缓存,不会自动更新。这个参数在 ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 稍微有些差异,会具体说下。
    • fallbackToSystemLocale:这是一个兜底开关。默认情况下,如果在指定语言中找不到对应的值,会从 basename 参数(默认是 messages.properties)中查找,如果再找不到可能直接返回或抛错。该参数设置为 true 的话,还会再走一步兜底逻辑,从当前系统语言对应配置文件中查找。该参数默认是 true;
    • alwaysUseMessageFormat:MessageSource 组件通过MessageFormat.format函数对国际化信息格式化,如果注入参数,输出结果是经过格式化的。比如MessageFormat.format("Hello, {0}!", "Kanshan")输出结果是“Hello, Kanshan!”。该参数控制的是,当输入参数为空时,是否还是使用MessageFormat.format函数对结果进行格式化,默认是 false;
    • useCodeAsDefaultMessage:当没有找到对应信息的时候,是否返回 code。也就是当找了所有能找的配置文件后,还是没有找到对应的信息,是否直接返回 code 值。默认是 false,即不返回 code,抛出NoSuchMessageException异常。

    这些配置参数都有各自的默认值。如果没有特殊的需求,可以直接直接按照默认约定使用。

    执行逻辑

    接下来我们看下流程图,下面的流程图绿色部分是 cacheDuration 没有配置的情况。对于 ResourceBundleMessageSource 是只加载一次配置文件,ReloadableResourceBundleMessageSource 会根据文件修改时间判断是否需要重新加载。

    ResourceBundleMessageSource 的流程图

    ResourceBundleMessageSource

    ReloadableResourceBundleMessageSource 的流程图

    ReloadableResourceBundleMessageSource

    AbstractMessageSource 的几个 getMessage 方法源码

    @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;
        }
        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;
        }
        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 {
        String[] codes = resolvable.getCodes();
        if (codes != null) {
            for (String code : codes) {
                String message = getMessageInternal(code, resolvable.getArguments(), locale);
                if (message != null) {
                    return message;
                }
            }
        }
        String defaultMessage = getDefaultMessage(resolvable, locale);
        if (defaultMessage != null) {
            return defaultMessage;
        }
        throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    第一个getMessage方法,是可以传入默认值defaultMessage的,也就是当所有 basename 的配置文件中不存在 code 指定的值,就会使用defaultMessage值进行格式化返回。

    第二个getMessage方法,是通过判断useCodeAsDefaultMessage配置,如果设置了 true,在所有 basename 的配置文件中不存在 code 指定的值的情况下,会返回 code 作为返回值。但是当设置为 false 时,code 不存在的情况下,会抛出NoSuchMessageException异常。

    第三个getMessage方法,传入的是MessageSourceResolvable接口对象,查找的 code 更加多种多样。不过如果最后还是找不到,会抛出NoSuchMessageException异常。

    缓存的使用

    我们看源码不仅仅是为了看功能组件的实现,还是学习更加优秀的编程方式。比如下面这段内存缓存的使用,Spring 源码中很多地方都用到了这种内存缓存的使用方式:

    // 两层 Map,第一层是 basename,第二层是 locale
    private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =
            new ConcurrentHashMap<>();
    
    @Nullable
    protected ResourceBundle getResourceBundle(String basename, Locale locale) {
        if (getCacheMillis() >= 0) {
            // Fresh ResourceBundle.getBundle call in order to let ResourceBundle
            // do its native caching, at the expense of more extensive lookup steps.
            return doGetBundle(basename, locale);
        }
        else {
            // Cache forever: prefer locale cache over repeated getBundle calls.
            // 先从缓存中获取第一层 basename 的缓存
            Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
            if (localeMap != null) {
                // 如果命中第一层,在通过 locale 获取第二层的值
                ResourceBundle bundle = localeMap.get(locale);
                if (bundle != null) {
                    // 如果命中第二层缓存,直接返回
                    return bundle;
                }
            }
            try {
                // 走到这里,说明没有命中缓存,就根据 basename 和 locale 创建对象
                ResourceBundle bundle = doGetBundle(basename, locale);
                if (localeMap == null) {
                    // 如果 localeMap 为空,说明第一级就不存在,通过 Map 的 computeIfAbsent 方法初始化
                    localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
                }
                // 将新建的 ResourceBundle 对象放入 localeMap 中
                localeMap.put(locale, bundle);
                return bundle;
            }
            catch (MissingResourceException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
                }
                // Assume bundle not found
                // -> do NOT throw the exception to allow for checking parent message source.
                return null;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    还有一种使用 Map 实现内存缓存的写法,比如我们就对上面的这个方法进行改写:

    public class ResourceBundleMessageSourceExt extends ResourceBundleMessageSource {
        private final Map<BasenameLocale, ResourceBundle> cachedResourceBundles = new ConcurrentHashMap<>();
    
        @Override
        protected ResourceBundle getResourceBundle(String basename, Locale locale) {
            if (getCacheMillis() >= 0) {
                // Fresh ResourceBundle.getBundle call in order to let ResourceBundle
                // do its native caching, at the expense of more extensive lookup steps.
                return doGetBundle(basename, locale);
            } else {
                // Cache forever: prefer locale cache over repeated getBundle calls.
                final BasenameLocale basenameLocale = new BasenameLocale(basename, locale);
                ResourceBundle resourceBundle = this.cachedResourceBundles.get(basenameLocale);
                if (resourceBundle != null) {
                    return resourceBundle;
                }
                try {
                    ResourceBundle bundle = doGetBundle(basename, locale);
                    this.cachedResourceBundles.put(basenameLocale, bundle);
                    return bundle;
                } catch (MissingResourceException ex) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
                    }
                    // Assume bundle not found
                    // -> do NOT throw the exception to allow for checking parent message source.
                    return null;
                }
            }
        }
    
        public record BasenameLocale(String basename, Locale locale) {
            @Override
            public boolean equals(Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }
                BasenameLocale that = (BasenameLocale) o;
                return basename.equals(that.basename) && locale.equals(that.locale);
            }
    
            @Override
            public int hashCode() {
                return Objects.hash(basename, locale);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    我们可以利用 Map 是通过equals判断 key 是否一致的原理,创建一个包含 basename、locale 的对象BasenameLocale,然后改写cachedResourceBundles为一层 Map,会简化一些判断逻辑。

    此处的BasenameLocalerecord类型,具体语法可以参考 Java16 的新特性 中的 Record 类型一节。

    文末总结

    本文先介绍了 MessageSource 的配置项,然后通过流程图的方式介绍了ResourceBundleMessageSourceReloadableResourceBundleMessageSource的执行逻辑,最后分享了两个使用 Map 实现内存缓存的方式。

    下一节我们将扩展 MessageSource,实现从 Nacos 加载配置内容,同时实现动态修改配置内容的功能。

    本文中的实例已经传到 GitHub,关注公众号「看山的小屋」,回复spring获取源码。

    青山不改,绿水长流,我们下次见。

    推荐阅读


    你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

    个人主页:https://www.howardliu.cn
    个人博文:SpringBoot 实战:国际化组件 MessageSource 的执行逻辑与源码
    CSDN 主页:https://kanshan.blog.csdn.net/
    CSDN 博文:SpringBoot 实战:国际化组件 MessageSource 的执行逻辑与源码

    👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻
  • 相关阅读:
    开源框架面试之MyBatis面试题
    Vue2-父子组件传值
    【python基础】类详解:如何编写类、__init__()、修改实例属性、类存储到模块并导入、py标准库、编写类的约定
    《Java 核心技术卷1 基础知识》第三章 Java 的基本程序设计结构 笔记
    6.DesignForPlacement\ExportHighlightedList
    2023年中国调速器产量、销量及市场规模分析[图]
    ThingsBoard源码解析-规则引擎
    JMeter参数化方式:三招让你的性能测试更灵活!
    Linux进程间通信(一)
    doris operator部署Doris集群教程
  • 原文地址:https://blog.csdn.net/conansix/article/details/126217282