4.4.2 资源包
资源包(resource bundle)应该是开发人员最熟悉的对Java程序进行国际化的方式。通常的做法是把程序中要显示给用户的消息文本都抽取出来,保存在属性文件中。这样的属性文件通常有一组,包含的内容是相似的,只是每个属性文件分别对应于一个特定的区域设置。资源包在使用方式上类似于一个java.util.Map接口,即其中所包含的是键值对的列表。从属性文件中创建出来的资源包自动地包含文件中声明的键值对作为其内容。使用者只需要根据当前的区域设置获取到对应的资源包对象,再从其中根据当前要显示的消息的键来获取消息的内容并显示出来即可。
Java中的java.util.ResourceBundle类用来表示一个资源包。每个资源包都有两个重要的属性:一个是它的基本名称,用来区分不同的资源包;另外一个是资源包对应的Locale类的对象。基本名称相同的所有资源包所包含的键是相同的,区别在于键的值是否经过本地化处理,以对应于不同的Locale类的对象所表示的区域设置。前面提到的基于属性文件的资源包只是资源包的一种形式,由java.util.PropertyResourceBundle类来表示,其中只能包含字符串作为键的值;另外一种形式是直接继承自ResourceBundle类的Java类,其中可以包含任意Java对象作为键的值。这两种形式其实是统一的,对于属性文件,在获取的时候会自动从文件中创建PropertyResourceBundle类的对象。在程序中使用资源包的时候,总是使用ResourceBundle类及其子类的对象。
如果希望创建自己的ResourceBundle类的实现,可以直接继承ResourceBundle类,或使用java.util.ListResourceBundle类。在直接继承ResourceBundle类时,只需要实现用来遍历其中包含的所有键的getKeys方法,以及根据键来获取对应值的handleGetObject方法即可。ListResourceBundle类则提供了一种基于二维数组的快速创建ResourceBundle类的对象的实现。这个二维数组的每一行表示一条记录,而对应的第一列和第二列分别是键和值。只需要继承此类,实现其中的getContents方法来返回一个包含了全部数据的二维数组即可。ListResourceBundle类非常适合在内存中创建和维护资源包或是作为其他ResourceBundle类格式的包装容器。
在创建了程序中所需的属性文件或ResourceBundle类的子类之后,下一步是找到适合于当前用户的区域设置的ResourceBundle类的对象。这是通过ResourceBundle的getBundle方法的各种不同的重载方式来实现的。这些不同的实现方式分成两类:第一类是Java 6之前的基于资源包的基本名称、Locale类的对象和类加载器对象的查找方式;第二类是Java 6中新增的通过ResourceBundle.Control类的对象来控制查找过程的查找方式。
第一种查找方式的关键在于根据资源包的基本名称和Locale类的对象来生成一个资源包对应的Java类和属性文件的名称查找序列。这个名称查找序列会考虑到Locale类的对象中的语言、书写方式、国家或地区,以及变体等信息,其基本的思想是优先查找最具体的资源包,如果找不到,就尝试查找通用一些的资源包。如果在尝试了指定的Locale类的对象之后,仍然无法找到对应的资源包,这个过程会以当前Java虚拟机的默认的Locale类的对象为目标,再重复执行一次。如果仍然无法找到,就会抛出java.util.MissingResourceException异常。例如,假设基本名称是“Messages”,所用的Locale类的对象的语言和国家或地区分别是“en”和“US”,那么对应的名称查找序列是:Messages_en_US、Messages_en和Messages。对于这些候选名称,首先尝试通过指定的类加载器来查找并加载对应名称的Java类,如果这个过程成功并且该Java类可以转换成ResourceBundle类,就创建该类的一个实例,作为查找到的结果。如果查找Java类的过程失败,接着会尝试查找属性文件。由于基本名称中可能带有名称空间,对应的属性文件名称是把候选名称中的“.”替换成“/”之后的名称,再通过类加载器的getResource方法来进行查找。如果找到属性文件,会以此文件作为输入来创建一个PropertyResourceBundle类的对象作为结果。
ResourceBundle类的对象本身也是存在一定层次结构的。一个ResourceBundle类的对象有可能存在一个父ResourceBundle类的对象。子ResourceBundle类的对象中会包含父ResourceBundle类的对象中定义的键值对。这种层次关系只有在资源包的查找过程中才会建立。根据上面提到的查找时的候选名称序列,出现在后面的查找到的ResourceBundle类的对象是之前的ResourceBundle类的对象的父亲。在查找过程中,会对所有能够成功创建的ResourceBundle类的对象建立这种层次结构关系。代码清单4-9给出了资源包的查找过程和基本的使用方式。对于同一基本名称的资源包,代码中分别使用Java类和属性文件来提供对应不同区域设置的本地化内容。查找到ResourceBundle类的对象之后,通过其getString方法来获取本地化的内容。
代码清单4-9 资源包的查找过程和基本的使用方式
//Java类定义的ResourceBundle
public class Messages_en_US extends ListResourceBundle{
public Object[][]getContents(){
return new Object[][]{
{"GREETING","Hello!"},
{"THANK_YOU","Thank you!"}
};
}
}
//属性文件
GREETING=你好!
THANK_YOU=谢谢!
//使用ResourceBundle的代码
public void useResourceBundle(){
String baseName="com.java7book.chapter4.resourcebundle.Messages";
ResourceBundle bundleEn=ResourceBundle.getBundle(baseName, Locale.US);
bundleEn.getString("GREETING");//值为“Hello!”
ResourceBundle bundleCn=ResourceBundle.getBundle(baseName, Locale.CHINA);
bundleCn.getString("THANK_YOU");//值为“谢谢!”
}
在第一种查找过程中,开发人员所能控制的地方很少。为了满足开发人员的需求,Java 6中引入了ResourceBundle.Control类来允许开发人员对ResourceBundle的查找过程进行复杂的定制,同时也对查找过程进行了增强。开发人员可以通过继承ResourceBundle.Control类的方式来定制自己所需的行为。实际上,从Java 6之后,第一种查找方式的内部实现也用了ResourceBundle.Control类,只不过用的是该类的默认实现,以符合Java 6之前的查找过程。在ResourceBundle.Control类中可以定制的部分包括以下几个方面:
首先是ResourceBundle类的对象的类型。通过ResourceBundle.Control类的getFormats方法可以返回对于给定的基本名称应该要查找的ResourceBundle类的对象的格式名称。前面提到的Java类和属性文件等两种类型的名称分别是“java.class”和“java.properties”。开发人员可以选择只查找这两种基本格式中的一种,或者使用自定义的格式名称。
第二个可以定制的是在对指定的Locale类的对象进行查找时要搜索的Locale类的对象的列表。这个Locale类的对象的列表的作用等价于第一种查找方式中的候选名称列表。通过覆写getCandidateLocales方法来为每个基本名称和指定的Locale类的对象提供一组Locale类的对象作为候选名称。
第三个可以定制的是当通过给定的Locale类的对象找不到资源包时应该要尝试使用的Locale类的对象。第一种查找方式在这种情况下使用的是Java虚拟机的默认Locale类的对象。通过覆写getFallbackLocale方法可以改变这种行为,以合适的Locale类的对象来替代。
第四个可以定制的是ResourceBundle类的对象的缓存行为。在默认情况下,ResourceBundle类的对象在查找完成并设置好父ResourceBundle类的对象之后会被缓存起来。当下次再查找的时候,会直接使用缓存中的对象。通过第一种查找方式无法对缓存进行控制,而ResourceBundle.Control类则提供了相关的缓存控制机制。可以为在缓存中的ResourceBundle类的对象指定一个存活时间(time to live)。通过覆写getTimeToLive方法可指定这个时间。如果在调用getBundle方法的时候发现缓存中的ResourceBundle类的对象已经超过了其存活时间,会调用ResourceBundle.Control类的needsReload方法来判断是否需要重新创建。如果需要,getBundle方法会重新查找并创建新的ResourceBundle类的对象;否则还是会使用缓存中的对象并更新其存活时间。这个缓存机制与HTTP协议中的缓存机制是类似的。如果ResourceBundle类的对象还有存活时间,是不会通过needsReload方法来进行检查的。使用ResourceBundle类的clearCache方法可以清空缓存的ResourceBundle类的对象。
最后是通过newBundle方法来实际创建ResourceBundle类的对象。默认的实现只是提供了对Java类和属性文件的处理。如果采用的是自己的资源包格式名称,需要在这个方法中添加相应的创建逻辑。
在使用了ResourceBundle.Control类的对象的情况下,资源包的查找过程与采用第一种查找方式相比,整体的流程是相似的。只不过在某些关键步骤上getBundle方法会调用ResourceBundle.Control类中的特定方法来确定下一步的处理方式。在最开始的时候,getBundle方法会首先检查缓存。接着是通过getFormats方法来确定要查找的资源包的格式。然后通过getCandidateLocales方法来得到要搜索的Locale类的对象列表。列表中的Locale类的对象都会通过newBundle方法来尝试创建新的ResourceBundle类的对象。如果对上述Locale类的对象的尝试都失败,会通过getFallbackLocale方法来得到一个新的替代Locale类的对象,并再次尝试上面的查找过程。
下面通过一个具体的示例来说明ResourceBundle.Control类的用法和如何创建自己的资源包类型。前面提到过Java平台本身就支持Java类文件和属性文件两种资源包类型,而且Java类需要继承自ResourceBundle类。这里要创建的是一种基于任意Java类的资源包类型。这个Java类中公开的静态方法的名称作为资源包中的键,而方法调用的返回结果则作为键对应的值。使用第2章中介绍的反射API实现起来并不复杂。首先代码清单4-10中给出的ReflectiveResourceBundle类是自定义的资源包实现,其中的getKeys方法的实现是通过反射API得到当前类中所包含的所有不带参数的公开静态方法,而handleGetObject方法则根据键的值找到对应的方法,在执行方法调用之后返回结果。之所以使用静态方法,是因为在调用的时候不需要提供额外的接收者对象作为参数。
代码清单4-10 自定义的资源包实现
public class ReflectiveResourceBundle extends ResourceBundle{
private Class clazz;
public ReflectiveResourceBundle(Class clazz){
this.clazz=clazz;
}
public Object handleGetObject(String key){
if(key==null){
throw new NullPointerException();
}
try{
Method method=clazz.getMethod(key);
if(method==null){
return null;
}
return method.invoke(null);
}catch(Exception ex){
return null;
}
}
public Enumeration<String>getKeys(){
Vector<String>result=new Vector<String>();
Method[]methods=clazz.getMethods();
for(Method method:methods){
int mod=method.getModifiers();
if(Modifier.isStatic(mod)&&Modifier.isPublic(mod)&&method.getParameterTypes().length==0){
result.add(method.getName());
}
}
return result.elements();
}
}
接下来就是提供对应的ResourceBundle.Control类,如代码清单4-11所示。这里使用了“reflection”作为新资源包类型的名称。而在newBundle方法的实现中,只需要根据基本名称加载对应的Java类,再把该类对应的Class类的对象封装在ReflectiveResourceBundle类的对象中即可。
代码清单4-11 ResourceBundle.Control类的自定义子类
public class ReflectiveResourceBundleControl extends ResourceBundle.Control{
public List<String>getFormats(String baseName){
if(baseName==null){
throw new NullPointerException();
}
return Arrays.asList("reflection");
}
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)throws IllegalAccessException, InstantiationException, IOException{
if(baseName==null||locale==null
||format==null||loader==null){
throw new NullPointerException();
}
ResourceBundle bundle=null;
if(format.equals("reflection")){
String bundleName=toBundleName(baseName, locale);
try{
Class<?>clazz=loader.loadClass(bundleName);
return new ReflectiveResourceBundle(clazz);
}catch(ClassNotFoundException ex){
return bundle;
}
}
return bundle;
}
}
接着是以Java类的方式声明不同区域设置对应的资源包所包含的内容。每条记录对应一个类中的公开静态方法。代码清单4-12是一个简单的示例,其中只包含一个键“greet”,而该键的值是随机变化的。
代码清单4-12 具体的资源包中所包含的内容
public class ReflectiveMessages_zh_CN{
public static String greet(){
return"你好,"+(Math.random()>0.5?"先生":"女士");
}
}
最后是在实际中的应用,在代码清单4-13中可以看到,使用方式并没有很大的区别,只是在调用getBundle方法时多了一个ReflectiveResourceBundleControl类的对象而已。
代码清单4-13 自定义资源包实现的使用
public void useReflectiveResourceBundle(){
String baseName="com.java7book.chapter4.resourcebundle.ReflectiveMessages";
ReflectiveResourceBundleControl control=new ReflectiveResourceBundleContr ol();
ResourceBundle bundle=ResourceBundle.getBundle(baseName, Locale.CHINA, control);
Object value=bundle.getObject("greet");
}
得到了ResourceBundle类的对象之后,对它的使用主要是通过getString和getObject两个方法。对基于属性文件的ResourceBundle类的对象来说,只能通过getString方法来获取字符串格式的内容;而对于基于Java类的ResourceBundle类的对象来说,getObject方法也是可以使用的。在根据键来查找的时候,会考虑当前ResourceBundle类的对象在层次结构上的所有父ResourceBundle类的对象。如果从ResourceBundle类的对象中得到的字符串中包含占位符,可以通过下面介绍的消息格式化来进行处理。
在日常的开发中,基于属性文件的ResourceBundle类的对象的使用是最多的。在Java 6之前,属性文件对应的PropertyResourceBundle类的对象只能从InputStream类的对象中创建,而且对应的属性文件只能采用ISO 8859-1的编码格式。如果使用其他编码,会导致其中的内容无法识别。因此,无法在属性文件中直接使用非ISO 8859-1字符集中的字符。JDK自带的native2ascii工具可以把其他编码格式的属性文件转换成ISO 8859-1的格式。一般的做法是原始的属性文件本身使用UTF-8编码,由开发人员直接来修改。在构建过程中通过脚本的方式(如Apache Ant)调用native2ascii工具完成编码的转换。从Java 6开始,PropertyResourceBundle类也可以从java.io.Reader类的对象中创建,这就为其他编码格式的属性文件提供了便利。基于Java 6及其以后版本的程序利用从Reader类的对象创建的这种方式,可以简化构建的过程。代码清单4-14是一个使用了这种思想的ResourceBundle.Control类的子类。具体做法是,如果资源包的类型是“java.properties”,即表示属性文件,就从得到的InputStream类的对象中利用指定的编码格式创建一个Reader类的对象,再从该Reader类的对象中得到PropertyResourceBundle类的对象。
代码清单4-14 使用Reader类读取属性文件的ResourceBundle.Control类的子类
public class BetterResourceControl extends ResourceBundle.Control{
private String encoding="UTF-8";
public BetterResourceControl(String encoding){
if(encoding!=null){
this.encoding=encoding;
}
}
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)throws IllegalAccessException, InstantiationException, IOException{
if("java.properties".equals(format)){
String bundleName=toBundleName(baseName, locale);
String resourceName=toResourceName(bundleName,"properties");
InputStream stream=null;
if(reload){
URL url=loader.getResource(resourceName);
if(url!=null){
URLConnection connection=url.openConnection();
if(connection!=null){
connection.setUseCaches(false);
stream=connection.getInputStream();
}
}
}else{
stream=loader.getResourceAsStream(resourceName);
}
BufferedReader reader=new BufferedReader(new InputStream-Reader(stream, encoding));
return new PropertyResourceBundle(reader);
}
return super.newBundle(baseName, locale, format, loader, reload);
}
}
在程序中,可以把ResourceBundle.Control类的使用隐藏起来,提供一个封装好的工厂方法,如代码清单4-15所示。这样程序的其他部分只需要用UTF-8格式来编写属性文件,并用这个工厂方法来加载即可。
代码清单4-15 使用工厂方法封装对ResourceBundle.Control类的使用
public class ResourceBundleLoader{
public static ResourceBundle load(String baseName, Locale locale){
BetterResourceControl control=new BetterResourceControl(null);
return ResourceBundle.getBundle(baseName, locale, control);
}
}
通过这种方式,就省去了对native2ascii工具的使用,简化了构建过程。下面要介绍的是Java提供的对日期和时间、货币和数字以及消息的格式化和解析的支持。类java.text.Format及其子类用来完成相应的格式化和解析操作。格式化是通过format方法来实现,即把一个对象转换成字符串类型;而解析则通过parseObject方法来实现,即把一个字符串转换成对象。除了标准库中提供的格式化支持之外,程序也可以通过继承Format类来开发针对特定对象的格式化实现。