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

·  阅读 677

配置文件

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

  • 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 实现内存缓存的方式。

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

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改