1.国际化基础类
1.Locale
java.util.Locale是由JDK提供的本地方言类,本身内置了多个国家方言,我们可以通过此类来设置和获取方言,这些内置的国家方言为后面Resource Bundle的定义提供了标准,关于Resource Bundle的内容后面会讲到。以下代码展示了Locale代码类的基本使用。
public class LocaleDemo {
public static void main(String[] args) {
//获取默认方言,默认方言获取到的是系统语言
System.out.println(Locale.getDefault());
//修改系统方言
Locale.setDefault(Locale.US);
System.out.println(Locale.getDefault());
}
}
Locale内置的国家方言如下:
static public final Locale ENGLISH = createConstant("en", "");
static public final Locale FRENCH = createConstant("fr", "");
static public final Locale GERMAN = createConstant("de", "");
static public final Locale ITALIAN = createConstant("it", "");
static public final Locale JAPANESE = createConstant("ja", "");
static public final Locale KOREAN = createConstant("ko", "");
static public final Locale CHINESE = createConstant("zh", "");
static public final Locale SIMPLIFIED_CHINESE = createConstant("zh", "CN");
static public final Locale TRADITIONAL_CHINESE = createConstant("zh", "TW");
static public final Locale FRANCE = createConstant("fr", "FR");
static public final Locale GERMANY = createConstant("de", "DE");
static public final Locale ITALY = createConstant("it", "IT");
static public final Locale JAPAN = createConstant("ja", "JP");
static public final Locale KOREA = createConstant("ko", "KR");
static public final Locale CHINA = SIMPLIFIED_CHINESE;
static public final Locale PRC = SIMPLIFIED_CHINESE;
static public final Locale TAIWAN = TRADITIONAL_CHINESE;
static public final Locale UK = createConstant("en", "GB");
static public final Locale US = createConstant("en", "US");
static public final Locale CANADA = createConstant("en", "CA");
static public final Locale CANADA_FRENCH = createConstant("fr", "CA");
2.MessageFormat
java.text.MessageFormat类的作用是用来替换占位符的,类似于String.format(String pattern,Object...args)。例如我们在Resource Bundle中使用了"{0}"这样类似的占位符,那么就可以使用该类。
public class MessageFormatDemo {
public static void main(String[] args) {
MessageFormat messageFormat = new MessageFormat("hello,{0}");
System.out.println(messageFormat.format(new Object[]{"world"}));
}
}
2.ResourceBundle实现国际化
java.util.ResourceBundle是用来加载Resource Bundle资源的,根据Locale来进行国际化转化。但是因为默认properties文件是使用编码,所以只要出现中文就会乱码,下面的案例我们使用字符串编码的方式来解决中文乱码问题。
1.创建Resource Bundle
注意:这里添加Locales的时候,命令一定要和Locale内置的国家方言定义一致,如en,zh_CN,en_US等等,zh、en前面代表方言,CN、US后面代表国家。
language_en_US.properties:
name=test
args=hello,{0}
language_zh_CN.properties:
name=测试
args=你好,{0}
2.国际化
public class ResourceBundleDemo {
//Resource Bundle所在位置
private static final String BUNDLE_NAME = "i18n/language";
public static void main(String[] args) {
getEn();
getCn();
}
private static void getEn() {
Locale.setDefault(Locale.US);
ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_NAME);
String format = MessageFormat.format(bundle.getString("args"), new Object[]{"world"});
System.out.println(format);
}
private static void getCn() {
Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_NAME);
//因为properties文件,默认编码是ISO 8895-1,这里如果不进行编码转换,则会出现中文乱码
String name = bundle.getString("name");
System.out.println(name);
//字符串编码解决中文乱码
String format = MessageFormat.format(new String(bundle.getString("args").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8), new Object[]{"世界"});
System.out.println(format);
}
}
3.国际化乱码解决方案
上面的案例中,我们通过字符串编码的方式解决了中文乱码问题,但这治标不治本。下面我们将介绍其他三种解决Resource Bundle中文乱码问题的解决方案。
解决方案有以下三种:
1.采用jdk自带的native2ascii工具,将中文直接编码为ISO 8895-1放到properties文件中。
2.继承ResourceBundle.Control。
3.实现ResourceBundleControlProvider。
1.native2ascii
使用native2ascii命令,可以将我们properties文件中的中文转为ISO 8895-1编码,然后将新生成的文件的内容替换到原有的language_zh_CN.properties文件即可,再进行国际化读取的时候,就不会出现中文乱码了。
1.基本语法
native2ascii inputfile outputfile
2.生成的内容
name=\u6d4b\u8bd5
args=\u4f60\u597d,{0}
将生成的内容替换到language_zh_CN.properties文件即可。
3.测试
public class Native2Ascii {
public static void main(String[] args) {
ResourceBundle bundle = ResourceBundle.getBundle("i18n/language");
System.out.println(bundle.getString("name"));
}
}
2.继承ResourceBundle.Control
1.通过java.util.ResourceBundle.Control#newBundle可以看到ResourceBundel是由该方法生成的,核心代码如下:
public ResourceBundle newBundle(String baseName, Locale locale, String format,
ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
String bundleName = toBundleName(baseName, locale);
ResourceBundle bundle = null;
if (format.equals("java.class")) {
try {
@SuppressWarnings("unchecked")
Class extends ResourceBundle> bundleClass
= (Class extends ResourceBundle>)loader.loadClass(bundleName);
// If the class isn't a ResourceBundle subclass, throw a
// ClassCastException.
if (ResourceBundle.class.isAssignableFrom(bundleClass)) {
bundle = bundleClass.newInstance();
} else {
throw new ClassCastException(bundleClass.getName()
+ " cannot be cast to ResourceBundle");
}
} catch (ClassNotFoundException e) {
}
} else if (format.equals("java.properties")) {//会走这里
final String resourceName = toResourceName0(bundleName, "properties");
if (resourceName == null) {
return bundle;
}
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream stream = null;
try {
stream = AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public InputStream run() throws IOException {
InputStream is = null;
if (reloadFlag) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
// Disable caches to get fresh data for
// reloading.
connection.setUseCaches(false);
is = connection.getInputStream();
}
}
} else {
is = classLoader.getResourceAsStream(resourceName);
}
return is;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
if (stream != null) {
try {
//根据获取的流生成ResourceBundle
bundle = new PropertyResourceBundle(stream);
} finally {
stream.close();
}
}
} else {
throw new IllegalArgumentException("unknown format: " + format);
}
return bundle;
}
java.util.PropertyResourceBundle#PropertyResourceBundle(java.io.InputStream)
public PropertyResourceBundle (InputStream stream) throws IOException {
Properties properties = new Properties();
properties.load(stream);
lookup = new HashMap(properties);
}
通过debug我们会发现确实是在PropertyResourceBundle的构造方法中加载Stream的时候发生了乱码。
2.所以我们就可以通过继承java.util.ResourceBundle.Control来自定义一个Control,重新设置properties的编码格式。代码如下:
public class EncodeControl extends ResourceBundle.Control {
//由外部定义编码格式
private final String encode;
public EncodeControl(String encode) {
this.encode = encode;
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException {
String bundleName = toBundleName(baseName, locale);
ResourceBundle bundle = null;
if (format.equals("java.class")) {
try {
@SuppressWarnings("unchecked")
Class extends ResourceBundle> bundleClass
= (Class extends ResourceBundle>) loader.loadClass(bundleName);
// If the class isn't a ResourceBundle subclass, throw a
// ClassCastException.
if (ResourceBundle.class.isAssignableFrom(bundleClass)) {
bundle = bundleClass.newInstance();
} else {
throw new ClassCastException(bundleClass.getName()
+ " cannot be cast to ResourceBundle");
}
} catch (ClassNotFoundException e) {
}
} else if (format.equals("java.properties")) {
final String resourceName = toResourceName0(bundleName, "properties");
if (resourceName == null) {
return bundle;
}
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream stream = null;
try {
stream = AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public InputStream run() throws IOException {
InputStream is = null;
if (reloadFlag) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
// Disable caches to get fresh data for
// reloading.
connection.setUseCaches(false);
is = connection.getInputStream();
}
}
} else {
is = classLoader.getResourceAsStream(resourceName);
}
return is;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
if (stream != null) {
try {
//我们直接复制源码,修改如下两行代码即可,更改流的编码格式
Reader reader = new InputStreamReader(stream, encode);
bundle = new PropertyResourceBundle(reader);
} finally {
stream.close();
}
}
} else {
throw new IllegalArgumentException("unknown format: " + format);
}
return bundle;
}
private String toResourceName0(String bundleName, String suffix) {
// application protocol check
if (bundleName.contains("://")) {
return null;
} else {
return toResourceName(bundleName, suffix);
}
}
}
3.测试
public class EncodeControlDemo {
public static void main(String[] args) {
Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
//显式的指定我们使用的Control
ResourceBundle bundle = ResourceBundle.getBundle("i18n/language", new EncodeControl("UTF-8"));
System.out.println(bundle.getString("name"));
}
}
4.缺点
这种方式实现的最大问题在于不可移植,必须每次显式的指定Control。
3.实现ResourceBundleControlProvider
经过上面我们已经知道了java.util.ResourceBundle实例是由java.util.ResourceBundle.Control#newBundle生成的,而且我们通过继承java.util.ResourceBundle.Control重写它的newBundle方法,通过显式指定Control,解决了乱码问题。那么我们是不是可以有另一种思路,就是我们能不能不显式的指定的Control。基于此,我们在java.util.ResourceBundle中看到了如下源码:
static {
List list = null;
ServiceLoader serviceLoaders
= ServiceLoader.loadInstalled(ResourceBundleControlProvider.class);
for (ResourceBundleControlProvider provider : serviceLoaders) {
if (list == null) {
list = new ArrayList<>();
}
list.add(provider);
}
providers = list;
}
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;
}
从这两段源码中,我们可以看出来,java.util.ResourceBundle.Control是可以由java.util.spi.ResouceBundleControlProvider来生成的,而java.util.spi.ResourceBundleControlProvider是基于机制来加载它的实现类的,关于,可以参考这份博客。
1.implements ResouceBundleControlProvider
public class EncodeResourceBundleControlProvider implements ResourceBundleControlProvider {
@Override
public ResourceBundle.Control getControl(String baseName) {
//返回我们自己的Control
return new EncodeControl("utf-8");
}
}
2.创建SPI文件(文件名为接口的全路径名称)
文件内容(实现类的全路径名称):
com.ly.provider.EncodeResourceBundleControlProvider
这是SPI的规范,按照规范来就行了。
3.测试
public class ProviderDemo {
public static void main(String[] args) {
ResourceBundle bundle = ResourceBundle.getBundle("i18n/language");
System.out.println(bundle.getString("name"));
}
}
,究其原因是的问题。这里就涉及到SPI了,我就大概说一下,这里先看一段代码:
public class ProviderDemo {
public static void main(String[] args) {
ServiceLoader threadContextClassLoader = ServiceLoader.load(ResourceBundleControlProvider.class);
Iterator providerIterator = threadContextClassLoader.iterator();
System.out.println("-----------------线程上下文类加载器------------------");
while (providerIterator.hasNext()) {
System.out.println(providerIterator.next().toString());
}
ServiceLoader extensionClassLoader = ServiceLoader.loadInstalled(ResourceBundleControlProvider.class);
Iterator iterator = extensionClassLoader.iterator();
System.out.println("-----------------扩展类加载器------------------------");
while (iterator.hasNext()) {
System.out.println(iterator.next().toString());
}
}
}
运行结果如下:
从这个结果中,我们可以看出来java.util.ServiceLoader#load找到SPI的实现类,而java.util.ServiceLoader#loadInstalled找不到实现类,原因就是两个方法使用的不一样,查看java.util.ResourceBundle的源码:
从这个源码可以看出来,java.util.ResoureBundle调用的是java.util.ServiceLoader#loadInstalled,所以通过实现java.util.spi.ResourceBundleControlProvider的方式是不能解决乱码的。
其他相关文章:
Spring国际化使用教程
Spring解析Locale原理
Spring国际化消息解析原理