ResourceBundle热加载这个历史悠久的问题和陈年的老酒的唯一区别就是:老酒越老越香,ResourceBundle是越老越头疼。ReadOnly 老早之前就批判过ResourceBundle 愚蠢的设计,虽然当时有人认为那么设计合理,甚至还拿出了 SoftCache 这样的东西来辩驳,不过正确的东西永远是正确的,jdk1.6 中 ResourceBundle具备了 clearCache 的功能。那么对于要兼容 jdk1.6 以下的东西来说该怎么办呢?
Spring 中就有热加载的国际化方案,它是怎么做的?原来Spring 完全抛弃了 ResourceBundle,自己实现了一个国际化方案。好是挺好,不过我不能拿来直接用,因为目前的接口仍然是需要 ResourceBundle。
使用反射来修改ResourceBundle的cacheList的读取权限,然后清空cacheList,也是解决方案之一,不过并不是完全可靠。
好吧,不管怎么样,是重新写一个ResourceBundle,还是别的方式,在这之前我们都应该对 ResourceBundle有一个充分的了解。所以,我们还是先来看看ResourceBundle的源代码。考虑到兼容jdk1.4,所以我们就看 jdk1.4 的 ResourceBundle!
http://javaresearch.gro.clinux.org/jdk140/java/util/ResourceBundle.java.html 这个在线的代码非常好,大家可以参考这个。
ResourceBundle中首先是一堆变量的定义,一个一个来看看。
private static final ResourceCacheKey cacheKey =
new ResourceCacheKey();
这个 ResourceCacheKey 是什么呢?这是一个 private 的 class,看看它的定义:
private static final class ResourceCacheKey implements Cloneable {
....
}
我们需要关注的是这个方法:
public void setKeyValues(ClassLoader loader, String searchName,
Locale defaultLocale) {
this.searchName = searchName;
hashCodeCache = searchName.hashCode();
this.defaultLocale = defaultLocale;
if (defaultLocale != null) {
hashCodeCache ^= defaultLocale.hashCode();
}
if (loader == null) {
this.loaderRef = null;
} else {
loaderRef = new SoftReference(loader);
hashCodeCache ^= loader.hashCode();
}
}
似乎有点难懂...... 其实我们都知道ResourceBundle会缓存读取的properties文件,那么究竟是根据什么缓存的呢?其实就是靠这个 ResourceCacheKey 来缓存。
通过这个 setKeyValues 方法,可以知道 ResourceCacheKey 通过 ClassLoader, searchName(其实就是 bundle的名字),Locale 三个 hashcode 来合并成自己的 hashcode。然后, ResourceBundle 再用这个 ResourceCacheKey 来作为 key 缓存读取的信息。
我们也可以看看这个 ResourceCacheKey 的一些其他方法,如 equals,
如果以后我们也有这样的需求,这个 ResourceCacheKey 无疑是很好的范例。
现在再回到前面来:
private static final ResourceCacheKey cacheKey =
new ResourceCacheKey();
既然定义了一个 cacheKey ,而且是 static 的,就意味着 ResourceBundle 中不会再生成额外的 cacheKey 对象了。这一个 cacheKey 对象正是通过 setKeyValues 和 clear方法来不断重用的。
再继续看代码:
private static final int INITIAL_CACHE_SIZE = 25;
private static final float CACHE_LOAD_FACTOR = (float)1.0;
private static final int MAX_BUNDLES_SEARCHED = 3;
这是一堆 cache 相关的数字,没什么意义,掠过~~
private static final Hashtable underConstruction =
new Hashtable(MAX_BUNDLES_SEARCHED, CACHE_LOAD_FACTOR);
这个变量从名字上看来很难明白它的作用,没关系,到下面再看,掠过~~
private static final Integer DEFAULT_NOT_FOUND = new Integer(-1);
一个常量的定义,根据名字就知道,“默认没有对象”的定义,没什么意义,掠过~~
private static SoftCache cacheList =
new SoftCache (INITIAL_CACHE_SIZE, CACHE_LOAD_FACTOR);
终于看到关键的变量了,cacheList 正是缓存国际化信息的东西。
protected ResourceBundle parent = null;
parent?看来ResourceBundle还具有父子的关系(这个属性平时很少用哦)
private Locale locale = null;
很普通的定义,掠过~~
好了,类中定义的变量我们都分析完了,下面我们来看看 ResourceBundle 的方法。我们最常用的就是 bundle.getString 方法,那么就用这个方法当作入口来分析。
public final String getString(String key) {
return (String) getObject(key);
}
原来是继续调用了 getObject ,继续跟踪:
public final Object getObject(String key) {
Object obj = handleGetObject(key);
if (obj == null) {
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;
}
看来关键的就是这个 handleGetObject 方法了。
protected abstract Object handleGetObject(String key);
原来是一个抽象方法,头脑冲动,浪费时间!ResourceBundle的 javadoc 清清楚楚地讲了 ResourceBundle 的子类要重写 handleGetObject 方法。看来之前仔细的阅读javadoc还是有必要的阿!那就从 ResourceBundle.getBundle 开始吧!
public static final ResourceBundle getBundle(String baseName)
{
return getBundleImpl(baseName, Locale.getDefault(),
/* must determine loader here, else we break stack invariant */
getLoader());
}
public static final ResourceBundle getBundle(String baseName,
Locale locale)
{
return getBundleImpl(baseName, locale, getLoader());
}
public static ResourceBundle getBundle(String baseName, Locale locale,
ClassLoader loader)
{
if (loader == null) {
throw new NullPointerException();
}
return getBundleImpl(baseName, locale, loader);
}
其中有一个 getLoader 方法,这是一个得到 ClassLoader 的方法,里面又调用了一个 native 的方法,看不到代码,不过不重要,掠过~~
不过我们要注意到,这里出现了 ClassLoader , 不熟悉 ClassLoader 的同学快去补补课
好,接下来我们就来关注一下 getBundleImpl 这个方法。有点复杂,没关系,一点一点看。
private static ResourceBundle getBundleImpl(String baseName, Locale locale,
ClassLoader loader)
{
if (baseName == null) {
throw new NullPointerException();
}
//We use the class loader as the "flag" value that signifies a bundle
//that could not be found. This allows the entries to be garbage
//collected when the loader gets garbage collected. If we don't
//have a loader, use a default value for NOTFOUND.
// 这里定义了一个 NOTFOUND 变量
final Object NOTFOUND = (loader != null) ? (Object)loader : (Object)DEFAULT_NOT_FOUND;
//fast path the case where the bundle is cached
// 这里就是构造了 bundle 的名字,例如 baseName = country ,
// locale 是 Locale.CHINESE,
// 那么 bundleName 就会是 country_zh
String bundleName = baseName;
String localeSuffix = locale.toString();
if (localeSuffix.length() > 0) {
bundleName += "_" + localeSuffix;
} else if (locale.getVariant().length() > 0) {
//This corrects some strange behavior in Locale where
//new Locale("", "", "VARIANT").toString == ""
bundleName += "___" + locale.getVariant();
}
// The default locale may influence the lookup result, and
// it may change, so we get it here once.
Locale defaultLocale = Locale.getDefault();
// 从缓存中取 bundle ,根据 ClassLoader, bundleName,
// locale 来取,细节稍后分析。
Object lookup = findBundleInCache(loader, bundleName, defaultLocale);
// 注意:这里并不是 lookup == null, 而是上面定义的 NOTFOUND 变量。
if (lookup == NOTFOUND) {
throwMissingResourceException(baseName, locale);
} else if (lookup != null) {
// 如果缓存中存在,那么就使用缓存里的ResourceBundle对象。
return (ResourceBundle)lookup;
}
//The bundle was not cached, so start doing lookup at the root
//Resources are loaded starting at the root and working toward
//the requested bundle.
//If findBundle returns null, we become responsible for defining
//the bundle, and must call putBundleInCache to complete this
//task. This is critical because other threads may be waiting
//for us to finish.
// 这里定义了一个 parent 变量,根据名字很难判断它的作用,继续分析
Object parent = NOTFOUND;
try {
//locate the root bundle and work toward the desired child
// 查找 "root" bundle。啥是 root bundle?? 如果 baseName = country,
// 那么 root 找的就是 country.properties
Object root = findBundle(loader, baseName, defaultLocale, baseName, null, NOTFOUND);
if (root == null) {
// 如果没有找到 root bundle,那么就设置缓存中的 baseName 对应的
// bundle 对象为 NOTFOUND。
putBundleInCache(loader, baseName, defaultLocale, NOTFOUND);
root = NOTFOUND;
}
// Search the main branch of the search tree.
// We need to keep references to the bundles we find on the main path
// so they don't get garbage collected before we get to propagate().
// 这里调用了 calculateBundleNames 方法。这个 calculateBundleNames
// 就是计算有多少个可能存在的bundle 文件。
// 例如,baseName = country, 那么可能存在的属性文件有 country_zh,
// country_zh_CN 等等。
final Vector names = calculateBundleNames(baseName, locale);
// 初始化一个 Vector ,初始化后,之后对 Vector 的操作使用 addElement
// 就会提高效率。
Vector bundlesFound = new Vector(MAX_BUNDLES_SEARCHED);
// if we found the root bundle and no other bundle names are needed
// we can stop here. We don't need to search or load anything further.
// 这个地方简单,英文注释也很清晰:如果找到了 root bundle,而且没有其他
// 可能存在的 bundle 文件,那么就不用继续往下执行了。
boolean foundInMainBranch = (root != NOTFOUND && names.size() == 0);
// 如果没有 foundInMainBranch ,挺影响思维是不?
// 其实上面那个变量叫 notFoundInMainBranch ,而这个地方写成
// if(notFoundInMainBranch) 多好!
if (!foundInMainBranch) {
// 设置了 parent = root。这个 parent 到底干什么的还是没搞明白。
parent = root;
// 循环所有可能存在的 bundle 文件名字
for (int i = 0; i < names.size(); i++) {
bundleName = (String)names.elementAt(i);
// 查找 bundle 对象。其实这个地方是我最不明白的,既然这个
// names vector 是通过 locale 来
// 生成的,为什么查找 bundle 的时候要使用 defaultLocale 呢??
lookup = findBundle(loader, bundleName, defaultLocale, baseName, parent, NOTFOUND);
// 把 lookup 对象放入到 bundlesFound 里。注意:不管是不是
// 空都会放进去
bundlesFound.addElement(lookup);
if (lookup != null) {
// 如果 lookup 不是空,那么 parent = lookup, 然后
// 再循环... ... 有点迷糊了?没关系,我们假设自己
// 就是这个程序,来模拟运行一次就ok了!
// 假如 我们输入的 bundleName 是 country,而且
// cuntory.properties 存在,那么 root 就不是空,
// 那么 parent 也不是空。我们再假设 names 也不是空,
// 里面有两个成员,分别是 country_zh, country_zh_CN。
// 好,下面进入 这个 “for (int i = 0; i < names.size(); i++)”
// 循环。
// 先简单说一下 findBundle, findBundle 中会传入一个
// parent 参数,这个 parent 会设置成 返回结果的 parent。
// 晕了?好,继续。
// 第一次循环,假设存在 country_zh.properties。那么
// lookup 就不会是空的,
// 那么 lookup = country_zh.properties,
// parent = country_zh.properties,
// 同时 lookup.parent = country.properties。
// 第二次循环,假设存在 country_zh_CN.properties。
// 那么 lookup 就不会是空的。
// 那么 lookup = country_zh_CN.properties ,
// parent = country_zh_CN.properties ,
// 同时 lookup.parent = country_zh.properties,
// 如果我们再往上推溯,
// lookup.parent.parent = country.properties。
// 俺的头脑实在不发达,也只能推溯到两次循环了。所以,
// 在两次循环结束后,我们的 parent 变量成为什么样子了呢?
// parent = country_zh_CN.properties
// parent.parent = country_zh.properties
// parent.parent.parent = country.properties
// 怎么样,明白了吧?
parent = lookup;
foundInMainBranch = true;
}
}
}
// parent = root ?? 那上面的那些处理不是都白费了吗?
// 没关系,别忘了 bundlesFound.addElement(lookup);
parent = root;
if (!foundInMainBranch) {
//we didn't find anything on the main branch, so we do the fallback branch
// 再一次计算可能存在的 bundle 文件,不过这次不是使用 locale,而是 defaultLocale
final Vector fallbackNames = calculateBundleNames(baseName, defaultLocale);
for (int i = 0; i < fallbackNames.size(); i++) {
bundleName = (String)fallbackNames.elementAt(i);
if (names.contains(bundleName)) {
//the fallback branch intersects the main branch so we can stop now.
break;
}
lookup = findBundle(loader, bundleName, defaultLocale, baseName, parent, NOTFOUND);
if (lookup != null) {
parent = lookup;
} else {
//propagate the parent to the child. We can do this
//here because we are in the default path.
// 如果 lookup 等于空,那么就把 parent 作为
// bundleName 的缓存。为了形象点,我们
// 还是做一个模拟比较好。如果当前的环境是日语系统,
// 那么假设 fallbackNames 中有一个
// 成员,就是 country_ja ,而如果 country_ja.properties
// 不存在,那么就把 parent当成是
// 处理 country_ja 的对象。那么 parent 是什么呢?
// 如果只循环一次,那么 parent 就是 root,
// 如果循环多次呢?自己模拟看看吧 :-)
putBundleInCache(loader, bundleName, defaultLocale, parent);
}
}
}
//propagate the inheritance/fallback down through the main branch
// 终于到最后了,这个 propagate 是什么呢?
parent = propagate(loader, names, bundlesFound, defaultLocale, parent);
} catch (Exception e) {
//We should never get here unless there has been a change
//to the code that doesn't catch it's own exceptions.
cleanUpConstructionList();
throwMissingResourceException(baseName, locale);
} catch (Error e) {
//The only Error that can currently hit this code is a ThreadDeathError
//but errors might be added in the future, so we'll play it safe and
//clean up.
cleanUpConstructionList();
throw e;
}
if (parent == NOTFOUND) {
throwMissingResourceException(baseName, locale);
}
return (ResourceBundle)parent;
}
先来看看这个简单的 propagate 方法:
private static Object propagate(ClassLoader loader, Vector names,
Vector bundlesFound, Locale defaultLocale, Object parent) {
// 又是一个循环
for (int i = 0; i < names.size(); i++) {
final String bundleName = (String)names.elementAt(i);
// 拿出与 bundleName对应的ResourceBundle对象
final Object lookup = bundlesFound.elementAt(i);
if (lookup == null) {
// 这个代码应该很熟悉了,如果没有与 bundleName匹配的,那么就用 parent 对象。
putBundleInCache(loader, bundleName, defaultLocale, parent);
} else {
parent = lookup;
}
}
// 最后返回 parent。其实按照我的理解,这个变量叫
// child 更形象。为什么呢?
// 正如上边模拟的,如果 baseName = country,
// names = [ country_zh, country_zh_CN ],
// 而且三个属性文件都存在,那么这里返回的就是 country_zh_CN
// 属性文件的 ResourceBundle 对象。
return parent;
}
好了,我们终于从整体上把这个长长的 getBundleImpl分析完了。现在我们再来分析一下其中的一些方法。
首先是 findBundleInCache 这个方法。
private static Object findBundleInCache(ClassLoader loader, String bundleName,
Locale defaultLocale) {
//Synchronize access to cacheList, cacheKey, and underConstruction
synchronized (cacheList) {
// 构造一个 cacheKey
cacheKey.setKeyValues(loader, bundleName, defaultLocale);
// 根据 cacheKey 得到缓存的 ResourceBundle 对象
Object result = cacheList.get(cacheKey);
cacheKey.clear();
return result;
}
}
这个方法没什么复杂的,接下来看看 findBundle 方法。
private static Object findBundle(ClassLoader loader, String bundleName, Locale defaultLocale,
String baseName, Object parent, final Object NOTFOUND) {
Object result;
synchronized (cacheList) {
//check for bundle in cache
// 首先也是从 cache 中取缓存对象。为什么这里还要再次从
// cache 中取呢?赶紧再 看看 getBundleImpl 里调用
// findBundle 的地方就知道了。有点乱,是不?
// 为什么这里不用 findBundleInCache 方法呢?
// 请向上看:synchronized (cacheList)
// findBundleInCache里也用了 synchronized (cacheList),
// 所以不能用那个方法了。
// 确实够乱了。。。。
cacheKey.setKeyValues(loader, bundleName, defaultLocale);
result = cacheList.get(cacheKey);
if (result != null) {
cacheKey.clear();
return result;
}
// check to see if some other thread is building this bundle.
// Note that there is a rare chance that this thread is already
// working on this bundle, and in the process getBundle was called
// again, in which case we can't wait (4300693)
// underConstruction 终于登场了!这是一个类的变量,之前咱们看到过。
// 这个东西作用是什么呢?就是以 cacheKey 作为 key, Thread 作为 value。
// 如果从 underConstruction中得到的 Thread 和 当前的 Thread 不同,
// 那就说明别的线程正在构建这个 bundle 对象。
// 可见编写这个方法的人苦心,如此细微的地方也都考虑到了。
// 可是,可是,为什么 findBundleInCache方法中不用考虑这个东西呢??
Thread builder = (Thread) underConstruction.get(cacheKey);
boolean beingBuilt = (builder != null && builder != Thread.currentThread());
//if some other thread is building the bundle...
if (beingBuilt) {
//while some other thread is building the bundle...
while (beingBuilt) {
cacheKey.clear();
try {
//Wait until the bundle is complete
cacheList.wait();
} catch (InterruptedException e) {
}
// 不要以为 cacheKey 的设置是无用的,因为 cacheKey 是一个 static 变量,
// 所以别的线程很可能会在运行中的时候修改这个变量,所以我们必须设置。
// 真是复杂深奥的情况阿......想破了我的脑袋
cacheKey.setKeyValues(loader, bundleName, defaultLocale);
beingBuilt = underConstruction.containsKey(cacheKey);
}
//if someone constructed the bundle for us, return it
// 这又是一个复杂的结局。如果其他线程已经在 cacheList 中存放了 bundle 对象,就
// 返回这个缓存结果。简直就是在读一本推理小说。
// 但是,往上看看,synchronized (cacheList) ,cacheList 不是已经同步了吗??
// 这么说,在我锁住这个资源的时候,别的线程也能偷偷地修改这个资源。如果说别的线程
// 在修改cacheList 的时候,我只能等待,那么上面的 while (beginBuilt) 又有什么用呢?
// 嗯,看来到此处应该是玄幻小说了。
result = cacheList.get(cacheKey);
if (result != null) {
cacheKey.clear();
return result;
}
}
//The bundle isn't in the cache, so we are now responsible for
//loading it and adding it to the cache.
// 你以为是玄幻的结束?错,这里又是另一个玄幻的开始。
// 这里该往 underConstruction 里放东西了,告诉其他线程,我正在创建这个bundle对象,
// 请你们使用 while (beingBuilt) {} 方式进行等待,虽然你们锁住了 cacheList ,但是我
// 一样能修改.
final Object key = cacheKey.clone();
underConstruction.put(key, Thread.currentThread());
//the bundle is removed from the cache by putBundleInCache
cacheKey.clear();
}
//try loading the bundle via the class loader
// 好了,好了,推理以及玄幻部分结束,我们回到了真实的世界。
// 使用 loadBundle 方法载入 bundle 资源。
result = loadBundle(loader, bundleName, defaultLocale);
if (result != null) {
// check whether we're still responsible for construction -
// a recursive call to getBundle might have handled it (4300693)
boolean constructing;
synchronized (cacheList) {
cacheKey.setKeyValues(loader, bundleName, defaultLocale);
// 如果 underConstruction 中得到的线程 = 当前的线程,说明不是别的线程构造的。
// 可是,如果不是别的线程构造的,不是一直会在 while (beingBuilt) {}里呆着吗?
// 我冥思苦想,终于明白:在那个玄幻的部分中,别的线程没有构造 bundle 对象,等到这里的
// 时候,恰好别的线程开始构造了。作者的思维真是严密呀!
constructing = underConstruction.get(cacheKey) == Thread.currentThread();
cacheKey.clear();
}
if (constructing) {
// set the bundle's parent and put it in the cache
// 如果是当前线程在构造的话,那么就设置 parent,以及放入到 cache 中。
final ResourceBundle bundle = (ResourceBundle)result;
if (parent != NOTFOUND && bundle.parent == null) {
bundle.setParent((ResourceBundle) parent);
}
bundle.setLocale(baseName, bundleName);
putBundleInCache(loader, bundleName, defaultLocale, result);
}
}
// 总结一下这个方法,在这个方法里,作者给我们展示了多线程下使用同一个资源时的并行处理方式,
// 运用了推理,以及玄幻的方式给我们展示了一种新的思路。
return result;
}
继续来看看其中的 putBundleInCache 方法。
private static void putBundleInCache(ClassLoader loader, String bundleName,
Locale defaultLocale, Object value) {
//we use a static shared cacheKey but we use the lock in cacheList since
//the key is only used to interact with cacheList.
synchronized (cacheList) {
cacheKey.setKeyValues(loader, bundleName, defaultLocale);
cacheList.put(cacheKey.clone(), value);
underConstruction.remove(cacheKey);
cacheKey.clear();
//notify waiters that we're done constructing the bundle
// notify 谁呢?整个代码中也没见谁在 wait。难不成有些线程会自动 wait ???
// 玄幻,又见玄幻
cacheList.notifyAll();
}
}
现在大脑已经进入迷糊状态了,赶紧清醒清醒,看看 loadBundle 是怎么回事。
private static Object loadBundle(final ClassLoader loader, String bundleName, Locale defaultLocale) {
// Search for class file using class loader
// 这部分是为了载入 class 用的。其实 bundle 不仅仅可以是 .properties 文件,使用 class 也可以。
// 之前一直说的都是 .properties 文件,是因为大部分人都用这种方式。
try {
Class bundleClass;
if (loader != null) {
bundleClass = loader.loadClass(bundleName);
} else {
bundleClass = Class.forName(bundleName);
}
if (ResourceBundle.class.isAssignableFrom(bundleClass)) {
Object myBundle = bundleClass.newInstance();
// Creating the instance may have triggered a recursive call to getBundle,
// in which case the bundle created by the recursive call would be in the
// cache now (4300693). For consistency, we'd then return the bundle from the cache.
Object otherBundle = findBundleInCache(loader, bundleName, defaultLocale);
if (otherBundle != null) {
return otherBundle;
} else {
return myBundle;
}
}
} catch (Exception e) {
// 顺便多一句,如果我们使用的是 .properties文件,那么 100% 这里会出 Exception。
// 既然判断 .properties 文件存在不存在更容易,为什么不把下面的载入文件放到一开始呢?
} catch (LinkageError e) {
}
// Next search for a Properties file.
// 这里就是我们熟悉的 .properties 文件了
final String resName = bundleName.replace('.', '/') + ".properties";
// 安全检查。好多同学写的读取文件从来不干这个
InputStream stream = (InputStream)java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Object run() {
if (loader != null) {
return loader.getResourceAsStream(resName);
} else {
return ClassLoader.getSystemResourceAsStream(resName);
}
}
}
);
if (stream != null) {
// make sure it is buffered
stream = new java.io.BufferedInputStream(stream);
try {
// PropertyResourceBundle,ResourceBundle 的一个子类。
return new PropertyResourceBundle(stream);
} catch (Exception e) {
} finally {
try {
stream.close();
} catch (Exception e) {
// to avoid propagating an IOException back into the caller
// (I'm assuming this is never going to happen, and if it does,
// I'm obeying the precedent of swallowing exceptions set by the
// existing code above)
}
}
}
return null;
}
呼呼,好了,至此我们已经把 ResourceBundle 的主要内容分析完了!根据目前的代码,我们反向推理一下需求:
1、ResourceBundle可以载入 class 或者 .properties,而且可以传递 ClassLoader 来进行载入。
2、具有层次的功能。也就是 country.properties, country_zh.properties, country_zh_CN.properties 的读取方式。
优先读取最底级的,如果底级不存在,那么依次向上寻找。
3、具有缓存的功能。根据 ClassLoader, bundleName, locale 三个条件来缓存。
此外还具有一些普通人难以掌握和理解的东西:
1、多线程情况下构造 bundle 对象的处理。
2、多线程的并发处理。
(此为玄幻部分,如果不想写玄幻小说,无需钻研)
ResourceBundle代码摆在那,需求我们也能根据代码反推出来,那么解决热加载的方案呢?
很多人心中已经有数了,下次再一起讨论讨论。