国际化标准实现
Java 中的字符使用 Unicode 编码,因此支持各个国家的语言。如果开发的软件仅在中国使用,那么我们直接使用中文即可。如果开发的软件仅在美国使用,我们可以只使用英文。那如果我们开发的软件需要同时支持不同国家的语言呢?能否做到在不同的国家和地区使用我们开发的软件时展示相对应的语言?答案是肯定的,Java 已经进行了支持。为了支持国际化软件的开发,Java 提供了主要的两个类,分别是 Locale 和 ResourceBundle,下面加以介绍。
Java 国际化之 Locale
Java 提供了 java.util.Locale 类,表示特定的地理、政治或者文化区域。
有如下方式可以获取 Locale 的实例。
// 1. 获取系统默认 Locale,如 zh_CN,分别表示语言为 zh ,国家为 CN ;设置默认地区可调用方法 Locale#setDefault(Locale) Locale locale = Locale.getDefault(); // 2. 通过构造方法创建 Locale,language 表示语言,country 表示国家或者地区 public Locale(String language) public Locale(String language, String country) // 3. Locale 内置了一些常见的实例,部分示例如下 Locale.SIMPLIFIED_CHINESE Locale.ENGLISH Locale.JAPANESE Locale.KOREAN
Locale 作为地区信息通常不会直接使用,而是配合 Format 或者 ResourceBundle。Format 用于对文本进行格式化,而 ResourceBundle 用于根据地区信息和某一个 code 获取对应的文本值。
如果想打印出当前地区下的时间,我们可以使用如下的代码。
public class App { public static void main(String[] args) throws IOException { Locale locale = Locale.getDefault(); DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale); // 2020年11月21日 下午02时46分46秒 System.out.println(dateFormat.format(new Date())); } }
Java 国际化之 ResourceBundle
ResourceBundle 设计特性
ResourceBundle 作为一个抽象类,默认情况下读取类路径下用于国际化的 properties 资源文件。其设计具有如下特性。
基于 key-value 设计,可以根据 key 获取对应的 value。
层次性设计,当前 ResourceBundle 获取不到对应的 value 时将从父 ResourceBundle 查找。
缓存特性,其内部会对 key-value 进行缓存,以便下次直接获取。
提供 ResourceBundle.Control 自定义获取 ResourceBundle 实现。
通过 SPI 机制获取 ResourceBundleControlProvider 实现,用于获取 ResourceBundle.Control。
下面先看使用 ResourceBundle 直观的示例。
1. classpath 下新建目录 META-INF/ 2. META-INF/ 目录下新建文件 messages_en_US.properties ,内容如下: name = zzuhkp 3. META-INF/ 目录下新建文件 messages_zh_CN.properties ,内容如下: name = \u5927\u9e4f 4. 为了获取中文环境下 name 对应的值,示例如下。 public class App { public static void main(String[] args) throws IOException { ResourceBundle resourceBundle = ResourceBundle.getBundle("META-INF.messages", Locale.SIMPLIFIED_CHINESE); String name = resourceBundle.getString("name"); // name:大鹏 System.out.println("name:" + name); } }
这里需要注意的是获取 ResourceBundle 实例时提供的资源文件名的格式,目录和文件名使用.分隔,文件名不包含表示地区的部分以及文件类型。
如何获取 ResourceBundle 实例
ResourceBundle 是一个抽象类,同时其提供了工厂方法用于获取实现,具体如下。
public static final ResourceBundle getBundle(String baseName) public static final ResourceBundle getBundle(String baseName,Control control) public static final ResourceBundle getBundle(String baseName,Locale locale) public static final ResourceBundle getBundle(String baseName, Locale targetLocale,Control control) public static ResourceBundle getBundle(String baseName, Locale locale,ClassLoader loader) public static ResourceBundle getBundle(String baseName, Locale targetLocale,ClassLoader loader, Control control)
上述中的工厂方法getBundle
,参数少的方法将提供默认值直接调用参数最多的方法。如下所示。
public static final ResourceBundle getBundle(String baseName) { return getBundleImpl(baseName, Locale.getDefault(), getLoader(Reflection.getCallerClass()), getDefaultControl(baseName)); }
ResourceBundle 中的 Control
JDK 6 新增了一个类 ResourceBundle.Control 可用于自定义 ResourceBundle 的获取,如果使用了不带 Control 参数的工厂方法,将尝试进行查找,源码如下。
private static final List<ResourceBundleControlProvider> providers; static { List<ResourceBundleControlProvider> list = null; ServiceLoader<ResourceBundleControlProvider> serviceLoaders = ServiceLoader.loadInstalled(ResourceBundleControlProvider.class); for (ResourceBundleControlProvider provider : serviceLoaders) { if (list == null) { list = new ArrayList<>(); } list.add(provider); } providers = list; } // 获取默认的 Control private static Control getDefaultControl(String baseName) { if (providers != null) { for (ResourceBundleControlProvider provider : providers) { Control control = provider.getControl(baseName); if (control != null) { return control; } } } return Control.INSTANCE; }
可以看到,默认通过 SPI 机制获取的 ResourceBundleControlProvider 实例来获取 Control 实例,如果获取不到则会使用默认的 Control 实例。默认的 Control 实例支持两种策略来获取 ResourceBundle。
根据 baseName 获取 ResourceBundle 类名通过反射实例化获取 ResourceBundle 实例。
根据 baseName 获取对应的国际化资源文件,然后实例化 PropertyResourceBundle 类,前面中的示例就属于这种情况。
自定义获取 Control 获取 ResourceBundle
在前面的示例中我们看到 properties 文件中,我们使用了十六进制表示中文,这是因为默认情况下 properties 文件是使用 ISO-8859-1 编码进行读取,为了使用 UTF-8 读取 properties 文件,我们可以对前面的示例进行如下改造。
1. 修改 META-INF/messages_zh_CN.properties 内容如下: name = 中文名称:大鹏 2. 自定义 Control /** * @author zzuhkp * @date 2020-11-21 17:39 * @since 1.0 */ public class UTF8Control extends ResourceBundle.Control { @Override public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { String bundleName = this.toBundleName(baseName, locale); String resourceName = this.toResourceName(bundleName, "properties"); InputStream inputStream = loader.getResourceAsStream(resourceName); return new ResourceBundle() { private Map<String, Object> lookup; { Properties properties = new Properties(); properties.load(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); lookup = new HashMap(properties); } @Override protected Object handleGetObject(String key) { return lookup.get(key); } @Override public Enumeration<String> getKeys() { ResourceBundle parent = this.parent; return new ResourceBundleEnumeration(lookup.keySet(), (parent != null) ? parent.getKeys() : null); } }; } } 3. 修改示例代码如下。 public class App { public static void main(String[] args) throws IOException { ResourceBundle resourceBundle = ResourceBundle.getBundle("META-INF.messages", Locale.SIMPLIFIED_CHINESE,new UTF8Control()); String name = resourceBundle.getString("name"); System.out.println("name:" + name); } }
上述示例中,我们在获取 ResourceBundle 实例时提供了自定义的 Control 实例作为参数,打印结果如下。
name:中文名称:大鹏
Java 文本格式化
Java 文本格式化的核心类是 MessageFormat,它和国际化息息相关,因此这里也做提及。先通过一个样例学习其基本使用方式。
public class App { public static void main(String[] args) throws IOException { String message = "My name is {0},and my age is {1,number},and today is {2,date,full}"; MessageFormat messageFormat = new MessageFormat(message, Locale.getDefault()); String format = messageFormat.format(new Object[]{"zzuhkp", 26,new Date()}); // My name is zzuhkp,and my age is 26,and today is 2020年11月21日 星期六 System.out.println(format); } }
文本格式化即文本中存在占位符,在格式化时通过提供的参数及占位符指定的位置以及格式对其进行替换。
文本格式化的占位符格式为:{ArgumentIndex,[ArgumentType,[ArgumentStyle]]},其中[]内的为可选部分。
ArgumentIndex 表示参数的位置,从0开始。
ArgumentType 表示参数的格式类型,为可选项,可选值为"",、"number"、"date"、"time"、"choice"。
ArgumentStyle 表示参数的样式,为可选项,根据不同的样式格式化时会展示不同的内容,可选值为 ""、"short"、"medium"、"long"、"full"、"currency"、 "percent"、"integer"。
如果 MessageFormat 已经实例化,我们则可以对其进行重置相关信息,具体如下。
重置消息模式:java.text.MessageFormat#applyPattern
重置消息的区域信息:java.text.MessageFormat#setLocale
重置消息索引位置的格式:java.text.MessageFormat#setFormat
总结
本篇总结了 Java 对国际化的支持,包括 Locale、ResourceBundle 以及用于文本格式化的 MessageFormat,可以看到国际化文案的获取以及对文案的格式化并未整合到一起,下篇将分析 Spring 如何对这两者进行整合。