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

简介: 本章我们一起看下ResourceBundleMessageSource 和ReloadableResourceBundleMessageSource 的执行逻辑。SpringBoot 的 MessageSource 组件有很多抽象化,源码看起来比较分散,所以本文会通过流程图的方式进行讲解。

本章我们一起看下
ResourceBundleMessageSource

ReloadableResourceBundleMessageSource
的执行逻辑。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 的流程图

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);
}

复制代码

第一个 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;
        }
    }
}

复制代码

还有一种使用 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);
        }
    }
}

复制代码

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

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

文末总结

本文先介绍了 MessageSource 的配置项,然后通过流程图的方式介绍了
ResourceBundleMessageSource

ReloadableResourceBundleMessageSource
的执行逻辑,最后分享了两个使用 Map 实现内存缓存的方式。

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
2月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
106 5
|
3月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
277 2
|
20天前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
30天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
116 13
|
1月前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
2月前
|
前端开发 Java Spring
SpringBoot项目thymeleaf页面支持词条国际化切换
SpringBoot项目thymeleaf页面支持词条国际化切换
91 2
|
2月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
3月前
|
机器学习/深度学习 移动开发 自然语言处理
基于人工智能技术的智能导诊系统源码,SpringBoot作为后端服务的框架,提供快速开发,自动配置和生产级特性
当身体不适却不知该挂哪个科室时,智能导诊系统应运而生。患者只需选择不适部位和症状,系统即可迅速推荐正确科室,避免排错队浪费时间。该系统基于SpringBoot、Redis、MyBatis Plus等技术架构,支持多渠道接入,具备自然语言理解和多输入方式,确保高效精准的导诊体验。无论是线上医疗平台还是大型医院,智能导诊系统均能有效优化就诊流程。
|
缓存 Java Spring
Spring&SpringBoot源码笔记整理 |Bean的加载流程二
昨天的文章里提到Bean的加载流程和如何获取bean,今天继续源码解读。还是老规矩,看着Bean加载的时序图进入主题。
339 0
Spring&SpringBoot源码笔记整理 |Bean的加载流程二
|
缓存 Java Spring
Spring&SpringBoot源码笔记整理 |Bean的加载流程一
本篇文章主要介绍Bean的加载流程,以及Spring是怎么解决循环依赖的问题。
150 0
Spring&SpringBoot源码笔记整理 |Bean的加载流程一