ResourceBundle 是 Java 中用于国际化(i18n)和本地化(l10n) 的核心类,它允许应用程序根据不同的语言和地区动态加载外部资源(如文本、图像等),而无需修改代码。
本文章中介绍如何使用,并且会从源码的角度分析它是如何工作的。
public static void main(String[] args) {
// 按语言进行加载对应的resourceBundle
ResourceBundle bundle = ResourceBundle.getBundle("i18n.res", Locale.US);
// 从对应的resourceBundle中获取对应的key
System.out.println(bundle.getString("say.hello"));
}
源码分析
加载语言资源
public static final ResourceBundle getBundle(String baseName,
Locale locale)
{
return getBundleImpl(baseName, locale,
getLoader(Reflection.getCallerClass()),
getDefaultControl(baseName));
}
ResourceBundle.getBundle 调用了 方法 getBundleImpl
private static ResourceBundle getBundleImpl(String baseName, Locale locale,
ClassLoader loader, Control control) {
if (locale == null || control == null) {
throw new NullPointerException();
}
// 使用baseName, locale, loader进行创建缓存key
CacheKey cacheKey = new CacheKey(baseName, locale, loader);
ResourceBundle bundle = null;
// 从缓存中获取数据
BundleReference bundleRef = cacheList.get(cacheKey);
if (bundleRef != null) {
bundle = bundleRef.get();
bundleRef = null;
}
// 这里的bundle有可能空,因为BundleReference是一个软引用
if (isValidBundle(bundle) && hasValidParentChain(bundle)) {
return bundle;
}
// No valid bundle was found in the cache, so we need to load the
// resource bundle and its parents.
boolean isKnownControl = (control == Control.INSTANCE) ||
(control instanceof SingleFormatControl);
// 当前支持的格式类型:class 与 properties
// properties文件就是上面的res.properties文件
List<String> formats = control.getFormats(baseName);
if (!isKnownControl && !checkList(formats)) {
throw new IllegalArgumentException("Invalid Control: getFormats");
}
// 依据当前传入的语言或是已经配置的回退的语言(如系统语言)
ResourceBundle baseBundle = null;
for (Locale targetLocale = locale;
targetLocale != null;
targetLocale = control.getFallbackLocale(baseName, targetLocale)) {
// 从当前的语言获取候选的语言
// 如当前传入的是简体, 则会candidateLocales的内容为
// [zh_CN_#Hans, zh__#Hans, zh_CN, zh, ]
// 后面是前面的父bundle
List<Locale> candidateLocales = control.getCandidateLocales(baseName, targetLocale);
if (!isKnownControl && !checkList(candidateLocales)) {
throw new IllegalArgumentException("Invalid Control: getCandidateLocales");
}
// candidateLocales 倒序加载数据
// 当前以 PropertyResourceBundle 返回
bundle = findBundle(cacheKey, candidateLocales, formats, 0, control, baseBundle);
// If the loaded bundle is the base bundle and exactly for the
// requested locale or the only candidate locale, then take the
// bundle as the resulting one. If the loaded bundle is the base
// bundle, it's put on hold until we finish processing all
// fallback locales.
if (isValidBundle(bundle)) {
boolean isBaseBundle = Locale.ROOT.equals(bundle.locale);
if (!isBaseBundle || bundle.locale.equals(locale)
|| (candidateLocales.size() == 1
&& bundle.locale.equals(candidateLocales.get(0)))) {
break;
}
// If the base bundle has been loaded, keep the reference in
// baseBundle so that we can avoid any redundant loading in case
// the control specify not to cache bundles.
if (isBaseBundle && baseBundle == null) {
baseBundle = bundle;
}
}
}
if (bundle == null) {
if (baseBundle == null) {
throwMissingResourceException(baseName, locale, cacheKey.getCause());
}
bundle = baseBundle;
}
keepAlive(loader);
return bundle;
}
这里findBundle 方法的逻辑,可以深入再来看下
private static ResourceBundle findBundle(CacheKey cacheKey,
List<Locale> candidateLocales,
List<String> formats,
int index,
Control control,
ResourceBundle baseBundle) {
Locale targetLocale = candidateLocales.get(index);
ResourceBundle parent = null;
// 如果不是candidateLocales最后一个,则递归调用findBundle
// 也就是会以链表的形式,[zh_CN_#Hans, zh__#Hans, zh_CN, zh, ] 注意最后一个空的数据。
// 进行数据返回
// 最终的bundle 是 zh_CN_#Hans所属的bundle,parent会指向zh__#Hans所属的bundle
if (index != candidateLocales.size() - 1) {
parent = findBundle(cacheKey, candidateLocales, formats, index + 1,
control, baseBundle);
} else if (baseBundle != null && Locale.ROOT.equals(targetLocale)) {
return baseBundle;
}
// Before we do the real loading work, see whether we need to
// do some housekeeping: If references to class loaders or
// resource bundles have been nulled out, remove all related
// information from the cache.
Object ref;
while ((ref = referenceQueue.poll()) != null) {
cacheList.remove(((CacheKeyReference)ref).getCacheKey());
}
// flag indicating the resource bundle has expired in the cache
boolean expiredBundle = false;
// First, look up the cache to see if it's in the cache, without
// attempting to load bundle.
cacheKey.setLocale(targetLocale);
// 从缓存中获取bundle
ResourceBundle bundle = findBundleInCache(cacheKey, control);
if (isValidBundle(bundle)) {
expiredBundle = bundle.expired;
if (!expiredBundle) {
// If its parent is the one asked for by the candidate
// locales (the runtime lookup path), we can take the cached
// one. (If it's not identical, then we'd have to check the
// parent's parents to be consistent with what's been
// requested.)
if (bundle.parent == parent) {
return bundle;
}
// Otherwise, remove the cached one since we can't keep
// the same bundles having different parents.
BundleReference bundleRef = cacheList.get(cacheKey);
if (bundleRef != null && bundleRef.get() == bundle) {
cacheList.remove(cacheKey, bundleRef);
}
}
}
// 缓存中没有,则从资源中加载
if (bundle != NONEXISTENT_BUNDLE) {
CacheKey constKey = (CacheKey) cacheKey.clone();
try {
bundle = loadBundle(cacheKey, formats, control, expiredBundle);
// 加载成功,则放入缓存
if (bundle != null) {
if (bundle.parent == null) {
bundle.setParent(parent);
}
bundle.locale = targetLocale;
bundle = putBundleInCache(cacheKey, bundle, control);
return bundle;
}
// 如果加载没有资源,则放入一个空对象,避免重复加载
putBundleInCache(cacheKey, NONEXISTENT_BUNDLE, control);
} finally {
if (constKey.getCause() instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}
return parent;
}
findBundle的逻辑:按candidateLocales进行倒序加载数据,并且后面元素是前面元素的parent指向的数据。
获取资源
bundle.getString("say.hello")
public final String getString(String key) {
return (String) getObject(key);
}
public final Object getObject(String key) {
// 该方法可以被重载,可以看PropertyResourceBundle
Object obj = handleGetObject(key);
// 如果没有
if (obj == null) {
// 则从父bundle中找
// 按以上 [zh_CN_#Hans, zh__#Hans, zh_CN, zh, ]的顺序进行查找
if (parent != null) {
obj = parent.getObject(key);
}
if (obj == null) {
throw new MissingResourceException("Can't find resource for bundle "
+this.getClass().getName()
+", key "+key,
this.getClass().getName(),
key);
}
}
return obj;
}