百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-08-08 12:01 2 浏览

本章我们一起看下 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 的流程图

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 的配置项,然后通过流程图的方式介绍了 ResourceBundleMessageSourceReloadableResourceBundleMessageSource 的执行逻辑,最后分享了两个使用 Map 实现内存缓存的方式。

原文 https://xie.infoq.cn/article/fef4fcbd40e8eab75fac9de04

相关推荐

【Docker 新手入门指南】第十章:Dockerfile

Dockerfile是Docker镜像构建的核心配置文件,通过预定义的指令集实现镜像的自动化构建。以下从核心概念、指令详解、最佳实践三方面展开说明,帮助你系统掌握Dockerfile的使用逻...

Windows下最简单的ESP8266_ROTS_ESP-IDF环境搭建与腾讯云SDK编译

前言其实也没啥可说的,只是我感觉ESP-IDF对新手来说很不友好,很容易踩坑,尤其是对业余DIY爱好者搭建环境非常困难,即使有官方文档,或者网上的其他文档,但是还是很容易踩坑,多研究,记住两点就行了,...

python虚拟环境迁移(python虚拟环境conda)

主机A的虚拟环境向主机B迁移。前提条件:主机A和主机B已经安装了virtualenv1.主机A操作如下虚拟环境目录:venv进入虚拟环境:sourcevenv/bin/active(1)记录虚拟环...

Python爬虫进阶教程(二):线程、协程

简介线程线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能...

基于网络安全的Docker逃逸(docker)

如何判断当前机器是否为Docker容器环境Metasploit中的checkcontainer模块、(判断是否为虚拟机,checkvm模块)搭配学习教程1.检查根目录下是否存在.dockerenv文...

Python编程语言被纳入浙江高考,小学生都开始学了

今年9月份开始的新学期,浙江省三到九年级信息技术课将同步替换新教材。其中,新初二将新增Python编程课程内容。新高一信息技术编程语言由VB替换为Python,大数据、人工智能、程序设计与算法按照教材...

CentOS 7下安装Python 3.10的完整过程

1.安装相应的编译工具yum-ygroupinstall"Developmenttools"yum-yinstallzlib-develbzip2-develope...

如何在Ubuntu 20.04上部署Odoo 14

Odoo是世界上最受欢迎的多合一商务软件。它提供了一系列业务应用程序,包括CRM,网站,电子商务,计费,会计,制造,仓库,项目管理,库存等等,所有这些都无缝集成在一起。Odoo可以通过几种不同的方式进...

Ubuntu 系统安装 PyTorch 全流程指南

当前环境:Ubuntu22.04,显卡为GeForceRTX3080Ti1、下载显卡驱动驱动网站:https://www.nvidia.com/en-us/drivers/根据自己的显卡型号和...

spark+python环境搭建(python 环境搭建)

最近项目需要用到spark大数据相关技术,周末有空spark环境搭起来...目标spark,python运行环境部署在linux服务器个人通过vscode开发通过远程python解释器执行代码准备...

centos7.9安装最新python-3.11.1(centos安装python环境)

centos7.9安装最新python-3.11.1centos7.9默认安装的是python-2.7.5版本,安全扫描时会有很多漏洞,比如:Python命令注入漏洞(CVE-2015-2010...

Linux系统下,五大步骤安装Python

一、下载Python包网上教程大多是通过官方地址进行下载Python的,但由于国内网络环境问题,会导致下载很慢,所以这里建议通过国内镜像进行下载例如:淘宝镜像http://npm.taobao.or...

centos7上安装python3(centos7安装python3.7.2一键脚本)

centos7上默认安装的是python2,要使用python3则需要自行下载源码编译安装。1.安装依赖yum-ygroupinstall"Developmenttools"...

利用本地数据通过微调方式训练 本地DeepSeek-R1 蒸馏模型

网络上相应的教程基本都基于LLaMA-Factory进行,本文章主要顺着相应的教程一步步实现大模型的微调和训练。训练环境:可自行定义,mac、linux或者window之类的均可以,本文以ma...

【法器篇】天啦噜,库崩了没备份(天啦噜是什么意思?)

背景数据库没有做备份,一天突然由于断电或其他原因导致无法启动了,且设置了innodb_force_recovery=6都无法启动,里面的数据怎么才能恢复出来?本例采用解析建表语句+表空间传输的方式进行...