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给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
7天前
|
Web App开发 编解码 Java
B/S基层卫生健康云HIS医院管理系统源码 SaaS模式 、Springboot框架
基层卫生健康云HIS系统采用云端SaaS服务的方式提供,使用用户通过浏览器即能访问,无需关注系统的部署、维护、升级等问题,系统充分考虑了模板化、配置化、智能化、扩展化等设计方法,覆盖了基层医疗机构的主要工作流程,能够与监管系统有序对接,并能满足未来系统扩展的需要。
33 4
|
26天前
|
缓存 前端开发 Java
【Java】仓库管理系统 SpringBoot+LayUI+DTree(源码)【独一无二】
【Java】仓库管理系统 SpringBoot+LayUI+DTree(源码)【独一无二】
|
1月前
|
前端开发 Java 关系型数据库
Springboot公交车路线管理系统 毕业设计-附源码
Springboot公交车路线管理系统 毕业设计-附源码
|
2月前
|
机器学习/深度学习 人工智能 自然语言处理
springboot基于人工智能和自然语言理解技术的医院智能导医系统源码
智能导诊系统可为患者提供线上挂号智能辅助服务,患者根据提示手动输入自己的基本症状,通过智能对话方式,该系统会依据大数据一步步帮助患者“诊断”,并最终推荐就医的科室和相关专家。患者可自主选择,实现“一键挂号”。这一模式将精确的导诊服务前置,从源头上让医疗服务更高效。
372 2
|
2月前
|
XML 缓存 算法
SpringBoot2 | SpingBoot FilterRegistrationBean 注册组件 | FilterChain 责任链源码分析(九)
SpringBoot2 | SpingBoot FilterRegistrationBean 注册组件 | FilterChain 责任链源码分析(九)
27 0
|
9天前
|
人工智能 移动开发 前端开发
Springboot医院智慧导诊系统源码:精准推荐科室
医院智慧导诊系统是在医疗中使用的引导患者自助就诊挂号,在就诊的过程中有许多患者不知道需要挂什么号,要看什么病,通过智慧导诊系统,可输入自身疾病的症状表现,或选择身体部位,在经由智慧导诊系统多维度计算,精准推荐科室,引导患者挂号就诊,实现科学就诊,不用担心挂错号。
19 2
|
10天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
151 10
|
10天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
11天前
|
存储 数据可视化 安全
Java全套智慧校园系统源码springboot+elmentui +Quartz可视化校园管理平台系统源码 建设智慧校园的5大关键技术
智慧校园指的是以物联网为基础的智慧化的校园工作、学习和生活一体化环境,这个一体化环境以各种应用服务系统为载体,将教学、科研、管理和校园生活进行充分融合。无处不在的网络学习、融合创新的网络科研、透明高效的校务治理、丰富多彩的校园文化、方便周到的校园生活。简而言之,“要做一个安全、稳定、环保、节能的校园。
37 6
|
13天前
|
消息中间件 运维 供应链
springboot区域云HIS医院信息综合管理平台源码
云HIS系统分为两个大的系统,一个是基层卫生健康云综合管理系统,另一个是基层卫生健康云业务系统。基层卫生健康云综合管理系统由运营商、开发商和监管机构使用,用来进行运营管理、运维管理和综合监管。基层卫生健康云业务系统由基层医院使用,用来支撑医院各类业务运转。
21 2