PropertyResourceBundle 是一種 Java 機制,用於從實際的 Java 代碼中分離出特定於語言環境的文本。當應用程序調用這些特定於語言環境的其中一個屬性時,與給定的用戶語言環境相關的一個文本文件就被打開並被閱讀。
從 Java 代碼中分離出對語言環境敏感的消息是處理國際化問題的關鍵。只要應用程序使用 PropertyResourceBundle 來獲取它特定於語言環境的屬性,就可以通過為所有受支持的語言環境提供屬性文本文件而將應用程序很容易地翻譯成任何語言。
維護大量的資源束會是一項十分艱巨的任務 ― 就象努力維護一大段代碼一樣令人生畏。那麼為什麼不應用一些在代碼中使用的原理來促進資源束的重用呢?
本文的重點是為 PropertyResourceBundle 引入繼承概念。以這種方式,公共束就可以被共享,而可以在不影響其它的束用戶的前提下,引入更多特定的束。
首先,我將概括地敘述一下最普遍使用的設計策略。
當前的策略及它們的問題
當前使用 PropertyResourceBundle 的設計通常遵循以下三個策略:
使用一個大的、囊括一切的束(所有應用程序組件都共享這個束)。
允許每一個應用程序組件擁有自己獨立的束,這些束不在應用程序間重用。
允許一些不具有重復屬性定義的資源束。
讓我們更詳細地看一下其中的每一種策略:
單束:不重用,維護代價高
第一種方法在有關設計時和運行時的資源束上都有很大的爭議。它並不支持重用,因為只存在一個束且對該束所做的任何更改都會影響到使用該束的其它組件。
這種方法中的任何屬性的重新定義都要求所有組件使用新版本,或者添加一個新屬性。無論如何,維護大量資源束會很快變得很吃力,而且消耗的時間比您所希望的要多。
更小的,獨立的束:不重用,使問題一致
第一種方法的另一種選擇是將單資源束分裂成原始束的一些子集。這種方法將使每一個束獨立於其它束;允許每一個束重新定義屬性以供自己使用。
這種設計不支持重用,因為每一個束都必須定義它自己的屬性。而且每一個束在其定義中都保留獨立性。
各個束之間保持一致現在變得更困難,因為每一個束都是獨立的。比如每一個束可能都定義一個帶有郵件地址的屬性。如果這個地址現在發生了更改,每一個與此郵件地址有關的束都必須改變。
特定於屬性的束:維護的高代價轉向代碼
第三種方法依賴於代碼開發人員要明確了解給定任何屬性時要轉向哪一個資源束。
使用郵件地址屬性示例(其中地址屬性只在一個資源束中定義)時,任何需要包含地址的應用程序組件都必須轉到定義地址的特定資源束。在這種模式下,應用程序組件必須確定給定一個鍵時,要轉到哪一個資源束。對資源束所做的任何更改都可能要求對使用它們的應用程序組件進行類似的更改。
使用這種方法時,維護問題真的剛好從資源束轉到獲取屬性的代碼上了。
為了尋求一個有效的解決方案,我們需要在這三種方法中權衡利弊。
解決方案:兩全其美
最好的解決方案應該可以最大限度地減少上面討論的方法中的缺點,同時具有每種方法中的優點。在 Java PropertyResourceBundle 現有的國際化能力中引入繼承,是解決這個問題的一種方法。
這種策略將最大限度地重用束,同時給予每一個應用程序組件足夠的自由,讓其為自己定義新的屬性或者重新定義現有的屬性。這種概念建立在現有的用於定義國際化束的 Java 功能上,這種國際化束可以轉入和轉出給定的語言環境。按照這種方法構建,您不必復制任何功能;軟件會簡單地重用提供的功能。
束可以在整個應用程序組件間重用,因為它們可以通過層次結構的方式被定義。組件可以選擇定義一些新屬性在組件中使用,同時從另一個束中繼承其它屬性。一個屬性只需定義一次。其它組件可以簡單地繼承這個屬性。
另外一種選擇是,一個組件可以決定繼承的屬性不適合自己使用,然後重新定義該屬性 ― 這種概念類似於面向對象編程中的覆蓋方法或屬性。
增加繼承還減少了維護工作,因為每個屬性都被定義在層次結構中明確的位置上。如圖 1 所示,被更改的屬性會影響從發生更改的束繼承而來的所有的束。
圖 1. 資源束繼承層次結構的示例
設計目的
為實現我們的策略,首先我們需要設計繼承的機制。在設計機制時要牢記,我們要設法盡量減少現有代碼和現有資源束的必需的更改。
為了使現有代碼使用繼承束,我們需要合並一些最小限度的更改,包括從 PropertyResourceBundle 到新的對象類型的轉換並使用一個新的 管理器類(稍後我會討論這個類)。
現有的資源束需要重新定義嗎?不。這種方法的一個顯著的副作用就是,現有的資源束可以變成整個繼承結構的基礎,而無須對這些資源束進行改變。現有的資源束可以按原樣使用。
另一個設計目的是為了能夠在代碼 內和代碼 外(在將列出一個給定束的關系的另一個文件中)都能定義束關系層次結構。這種方法為您提供了在代碼外定義關系的機會,這樣會使修改和維護更加容易。也可以在代碼內改變層次結構,這樣就可以在運行時為開發人員提供精細的控制。
實現設計
新實現的主要功能是提供描述束之間的關系的繼承機制。
與 Java 語言的 類繼承不同,每個新束都被允許從多個父束中邏輯地繼承屬性。
新類:InheritedPropertyResourceBundle
為實現這些思想,我們將創建一個繼承 java.util.ResourceBundle 類的新類。新類將保持對 PropertyResourceBundle 的單獨引用,還有對任意數量的父束(被 InheritedPropertyResourceBundle 的這個實例邏輯地繼承的束 ― 請參閱清單 1)的可選的引用。
清單 1. InheritedPropertyResourceBundle 類的開頭
/**
* The class for representing an inherited PropertyResourceBundle.
* This class incorporates all the features of the
* java.util.PropertyResourceBundle while adding the notion of
* inheritance. The inheritance relationships can be defined
* in-line, or they can be provided in a relationships file which
* is locatable by the given ClassLoader. This file must be
* named <resource_base_name>.relationships.
* @see java.util.PropertyResourceBundle
*/
public class InheritedPropertyResourceBundle extends ResourceBundle {
/**
* Describes the parent relationships to other
* Inherited PropertyResourceBundles. These parent bundles
* will be checked if any key cannot be found in this bundle.
*/
private Vector relationships = null;
/**
* The java.util.ResourceBundle that backs this instance.
* This bundle will always be checked first for any key.
*/
private ResourceBundle instance = null;
注意:請參閱 參考資料下載本文中使用的完整的源代碼。
如清單 2 所示,當 InheritedPropertyResourceBundle 按照給定的鍵查詢時,將首先檢查提供支持的 PropertyResourceBundle 來查找資源。
清單 2. 用給定的鍵檢查 PropertyResourceBundle
Object retVal = null;
try {
// check the PropertyResourceBundle for the resource.
retVal = instance.getObject(key);
} catch (MissingResourceException mre) {
//Do nothing here. This will cause a search of the parents
//of this inherited bundle.
}
如果提供支持的 PropertyResourceBundle 找不到資源,將按順序檢查父束,如清單 3 所示。
清單 3. 用給定的鍵檢查父束
if (retVal == null) {
// resource not found in PropertyResourceBundle, check all parents.
if (relationships != null) {
for (Enumeration e = relationships.elements(); e.hasMoreElements();) {
// attempt to get the resource from the current parent bundle.
retVal = ((InheritedPropertyResourceBundle)e.nextElement()).
handleGetObject(key);
if (retVal != null) return retVal;
}
}
}
我們的另一個設計目的是允許在代碼內和代碼外(在一個獨立的文件中)都能修改關系層次結構。(稍後我們會討論第二個問題。)
如清單 4 所示,為了允許改變層次結構,提供了一套修改器方法。這些方法允許在運行時添加和移去父束。
清單 4. 關系層次結構修改器方法
/**
* Adds the InheritedPropertyResourceBundle at the end of the parent
* list.
* @param r the resource bundle which will be added to the end
* of the parent list.
*/
public void addRelationship(InheritedPropertyResourceBundle r) {
relationships.add(r);
}
/**
* Adds the InheritedPropertyResourceBundle at the given location. All
* subsequent parents will be pushed down one space.
* @param r the resource bundle which will be added.
* @param l the insertion location of the resource bundle. For example,
* a value of 0 will place this parent bundle at the beginning of the list
* - it will be checked for any key first.
*/
public void addRelationship(InheritedPropertyResourceBundle r, int l) {
relationships.add(l, r);
}
/**
* Removes the resource bundle from the parent list.
*/
public void removeRelationship(InheritedPropertyResourceBundle r) {
relationships.remove(r);
}
新類:RelationshipLoader
我們創建了另一個類來處理從文件中載入父束的細節問題。我們這樣做是為了從 InheritedPropertyResourceBundle 的實現中分離出對平面文件進行語法分析的邏輯。
新類 RelationshipLoader 既處理文件位置邏輯,也實際解析文件。
RelationshipLoader 類尋找一個與 InheritedPropertyResourceBundle 同名,但帶有 .relationships 後綴的文件。例如,如果一個束是用基礎束名稱 resources.ProductNames 創建的, RelationshipLoader 類就會按照類裝載程序管理的搜索規則來搜索一個名為 resources/ProductNames.relationships 的文件。
因為關系是定義在全局級別上,而且與使用的語言環境無關,.relationships 文件不需要翻譯。
.relationships 文件最好與實際的 .properties 文件位於同一目錄下,所以全部的資源束文件都保存在明確的位置。如果 RelationshipLoader 沒有找到 .relationships 的資源束文件,它就假定束不是從任何父束中繼承下來的。它會和 PropertyResourceBundle 一樣運行。請參閱清單 5。
從實際的資源束中分離繼承關系定義允許當前所有的資源束按原樣使用 ― 在這種新模式下,不需任何修改就可以使用現有的資源束。
清單 5. 在 RelationshipLoader 中用關系文件初始化父束
/**
* Initializes all parent relationships defined in the
* relationships file. Subsequent to this method call,
* the createRelationships() method can be called to set
* up the parents in the resource bundle.
*/
protected void initRelationships() {
// all parent bundles defined in the relationships file.
relationships = new Vector();
// gets all parent bundle names from the file.
Enumeration e = getRelationshipNames();
while (e.hasMoreElements()) {
String curName = (String)e.nextElement();
// Gets the InheritedPropertyResourceBundle from the
// manager with parent bundle name and the locale
// and class loader used to initialize the bundle.
// Then adds it to the list of parent bundles.
relationships.add(InheritedBundleManager.getInstance().
getBundle(curName, locale, loader));
}
}
/**
* Adds relationships to the InheritedPropertyResourceBundle.
* @param source the InheritedPropertyResourceBundle.
*/
protected void createRelations(InheritedPropertyResourceBundle source) {
for (Enumeration e = relationships.elements(); e.hasMoreElements();) {
InheritedPropertyResourceBundle cur =
(InheritedPropertyResourceBundle)e.nextElement();
source.addRelationship(cur);
}
}
新類:InheritedBundleManager
最後一個新類是所有創建的 InheritedPropertyResourceBundle 的一個管理器類,名為 InheritedBundleManager 。 InheritedBundleManager 類是單元素 ― 在 Java 虛擬機中只有一個實例是有效的。使用這個管理器類的原因是要最大限度地減少內存中的資源束。這樣考慮一下:如果另一個束每次繼承父束時,都要創建一個新的父束,VM 中資源束的數量很快就會超出控制。
出於這個原因, InheritedBundleManager 類管理由 InheritedPropertyResourceBundle 創建的所有實例。每一個束名稱、語言環境和類裝載程序(語言環境和類裝載程序作為可選參數)只允許有一個 InheritedPropertyResourceBundle 的實例。
例如,第一次用缺省語言環境和類裝載程序請求 resources.ProductNames 束時,管理器會創建一個新的實例並將其放入緩沖區。當出現帶有相同參數的後繼請求時,緩沖的實例就會被返回。
所有的新代碼都通過 InheritedBundleManager 來獲取 InheritedPropertyResourceBundle 的一個實例。重要的是繼承束機制的用戶要做同樣的事。
缺陷
這種方法並非萬靈藥;它的缺點包括循環繼承、獲取資源束引用的問題和繼承了意外的類。讓我們更詳細地看一下其中的每一個缺陷。
循環繼承
當前的實現並不檢查循環繼承。例如,如果定義了兩個 InheritedPropertyResourceBundle ― 一個名為 resources.CompetitorProductNames ,另一個名為 resources.ProductName ― 兩者都被定義為邏輯地繼承另一個,解決方案就會失敗。
這種檢查被有意地留在了設計之外,因為它會降低性能,特別是當繼承樹比較大的時候。
如果清楚地定義了繼承層次結構,確保沒有形成循環繼承的相關問題,這個問題完全可以被消除。 可以向代碼添加檢查語句,如果添加的父束會引起循環繼承有關的問題,那麼一個異常將被拋出。因為所有的父束都是通過 InheritedPropertyResourceBundle 中的兩個 addRelationship() 方法之一添加的,可以在那裡添加檢查語句。
獲取資源束引用
獲取對資源束的引用的通常方法 ― 通過 ResourceBundle.getBundle() 方法 ― 不能在這種方法中使用,因為這些靜態的方法是硬編碼以返回一個特定的類型。換句話說,沒有辦法嵌入這種機制從而令人滿意地返回一個新 InheritedPropertyResourceBundle 類的實例。
要獲取對 InheritedPropertyResourceBundle 的一個引用,客戶機必須通過 InheritedBundleManager 接口。因此,您可能需要改變現有應用程序中的一些代碼。
意外的類繼承
InheritedPropertyResourceBundle 類的實現繼承 java.util.ResourceBundle 而不是 java.util.PropertyResourceBundle 。
為什麼? PropertyResourceBundle 的創建是與 ResourceBundle.getBundle() 方法緊密結合在一起的。如果實現反而繼承了 PropertyResourceBundle , ResourceBundle.getBundle() 中的大量現有邏輯將需要被管理器類重復。這就是為什麼必須更改一些代碼的另一個原因。
如果客戶機代碼需要 java.util.ResourceBundle 類型,這種解決方案不需要修改就可以很好地工作。另一方面,如果客戶機代碼需要 java.util.PropertyResourceBundle ,代碼就需要修改了。
結論
繼承方法有助於減輕與國際化開發工作有關的一些麻煩:
我們可以保留與資源束相關的當前功能,同時添加一些繼承語義。我們的方法允許編寫單獨的資源束來配合指定的應用程序,同時還允許用戶重用其它資源束來盡量避免資源重復。
由於在繼承層次結構的較高級別上對資源所做的更改會在較低的級別中反映出來,維護工作減輕了。
最後,我們最大限度地減少了對源代碼的更改,同時完全消除了修改現有資源束的需要。要將現有的資源束轉換成“繼承的”版本,只需在客戶機代碼中簡單地添加一個關系文件,或定義一個父束。
使用這裡描述的方法開發完全國際化的 Java 應用程序可以在很大程度上簡化您的設計和實現工作,同時使重用現有的束更加容易。
本文配套源碼