备注:JDK版本:1.8.0,系统环境:Ubuntu16.04 LTS
借助JDK进行国际化(多语言适配)操作,通常是在类路径(classpath)下定义不同语言的属性文件,以语言代码为后缀,将配置文件名称与语言代码以下划线的形式进行连接,比如简体中文的语言代码是zh_CN,自定义的属性文件名称为xxx_zh_CN.properties,然后借助java.util.ResourceBundle类自动基于不同的语言代码进行加载配置文件,以获取不同语言的配置信息。
下面我们先结合一个案例来引出使用ResourceBundle需要注意的点。
1. ResourceBundle的使用案例
1.1 语言代码的获取
JDK的java.util.Locale类,描述了常用的语言代码对应的英文编码信息,可以借助这个类,快速获取对应各语言代码信息
package com.zhru.wechat.jdk.international;import java.util.Locale;import java.util.stream.Stream;/*** Desp: 获取各个语言及其代码信息* 2019-10-25 21:35* Created by zhru.*/public class LocalInformationsDemo {public static void main(String[] args) {Stream.of(Locale.getAvailableLocales()).forEach(local ->System.out.println(local.getDisplayName() + ":" + local));}}
得到的结果如下:
......西班牙文 (波多黎哥):es_PR越南文 (越南):vi_VN英文 (美国):en_US中文 (中国):zh_CN日文 (日本):ja_JP德文 (希腊):de_GR塞尔维亚文 (拉丁文,塞尔维亚):sr_RS_#Latn希伯来文:iw英文 (印度):en_IN阿拉伯文 (黎巴嫩):ar_LB西班牙文 (尼加拉瓜):es_NI中文:zh......
1.2 定义属性文件
在系统类路径下定义不同语言的属性文件(UTF-8编码)
## myconfig_zh_CN.propertiesaddress=北京市
## myconfig_es_US.propertiesaddress=BeiJing Province
1.3 借助ResourceBundle进行加载
package com.zhru.wechat.jdk.international;import java.util.Locale;import java.util.ResourceBundle;/*** Desp: {@link ResourceBundle}加载配置信息* 2019-10-25 21:43* Created by zhru.*/public class ResourceBundleDemo {public static void main(String[] args) {/*** 以en_US语言编码进行加载配置*/ResourceBundle bundle = ResourceBundle.getBundle("myconfig", Locale.US);String address = bundle.getString("address");System.out.println("当前的语言编码是:" + Locale.US+ ",获取的address值:" + address);/*** 以zh_CN语言编码进行加载配置*/bundle = ResourceBundle.getBundle("myconfig", Locale.SIMPLIFIED_CHINESE);address = bundle.getString("address");System.out.println("当前的语言编码是:" + Locale.SIMPLIFIED_CHINESE+ ",获取的address值:" + address);}}
得到的结果如下:
/usr/lib/jdk1.8.0_171/bin/java当前的语言编码是:en_US,获取的address值:BeiJing Province当前的语言编码是:zh_CN,获取的address值:å京å¸Process finished with exit code 0
很明显,我们发现,即便使用了JDK提供的ResourceBundle的,对中文的获取依然会出现乱码的现象。
2. ResourceBundle加载中文配置乱码的原因分析
一切从代码出发,我们查看JDK源代码,一探究竟:
1)java.util.ResourceBundle
......@CallerSensitivepublic static final ResourceBundle getBundle(String baseName){return getBundleImpl(baseName, Locale.getDefault(),getLoader(Reflection.getCallerClass()),getDefaultControl(baseName));}private static ResourceBundle getBundleImpl(String baseName, Locale locale,ClassLoader loader, Control control) {// 检查locale和control信息,control接下来会讲述if (locale == null || control == null) {throw new NullPointerException();}// 初始化cacheKey,用于从缓存中查找是否有相应的ResourceBundle// 在bundle加载的过程中baseName和classloader都不能变化// 在使用这个变量之前locale必须已经设置CacheKey cacheKey = new CacheKey(baseName, locale, loader);ResourceBundle bundle = null;// 从缓存中查找BundleReference bundleRef = cacheList.get(cacheKey);if (bundleRef != null) {bundle = bundleRef.get();bundleRef = null;}// 校验bundle,通过直接返回if (isValidBundle(bundle) && hasValidParentChain(bundle)) {return bundle;}// 缓存中没有有效的bundle,我们需要自行加载boolean isKnownControl = (control == Control.INSTANCE) ||(control instanceof SingleFormatControl);List<String> formats = control.getFormats(baseName);if (!isKnownControl && !checkList(formats)) {throw new IllegalArgumentException("Invalid Control: getFormats");}ResourceBundle baseBundle = null;for (Locale targetLocale = locale;targetLocale != null;targetLocale = control.getFallbackLocale(baseName, targetLocale)) {List<Locale> candidateLocales = control.getCandidateLocales(baseName, targetLocale);if (!isKnownControl && !checkList(candidateLocales)) {throw new IllegalArgumentException("Invalid Control: getCandidateLocales");}// 获取bundle,需要关注的方法bundle = findBundle(cacheKey, candidateLocales, formats, 0, control, baseBundle);......}return bundle;}/*** 查找Bundle*/private static ResourceBundle findBundle(CacheKey cacheKey,List<Locale> candidateLocales,List<String> formats,int index,Control control,ResourceBundle baseBundle) {......// 先从缓存中查找ResourceBundle bundle = findBundleInCache(cacheKey, control);if (isValidBundle(bundle)) {expiredBundle = bundle.expired;if (!expiredBundle) {......}}if (bundle != NONEXISTENT_BUNDLE) {CacheKey constKey = (CacheKey) cacheKey.clone();try {// 加载bundlebundle = loadBundle(cacheKey, formats, control, expiredBundle);if (bundle != null) {if (bundle.parent == null) {bundle.setParent(parent);}bundle.locale = targetLocale;bundle = putBundleInCache(cacheKey, bundle, control);return bundle;}putBundleInCache(cacheKey, NONEXISTENT_BUNDLE, control);} finally {if (constKey.getCause() instanceof InterruptedException) {Thread.currentThread().interrupt();}}}return parent;}/*** 加载Bundle*/private static ResourceBundle loadBundle(CacheKey cacheKey,List<String> formats,Control control,boolean reload) {// 加载bundle,获取locale信息Locale targetLocale = cacheKey.getLocale();ResourceBundle bundle = null;int size = formats.size();for (int i = 0; i < size; i++) {String format = formats.get(i);try {// 借助control生成bundle,核心方法bundle = control.newBundle(cacheKey.getName(), targetLocale, format,cacheKey.getLoader(), reload);} catch (LinkageError error) {cacheKey.setCause(error);} catch (Exception cause) {cacheKey.setCause(cause);}if (bundle != null) {// 设置cacheKey到format中,以便下次加载使用cacheKey.setFormat(format);bundle.name = cacheKey.getName();bundle.locale = targetLocale;bundle.expired = false;break;}}return bundle;}......
上面的代码我们可以发现,真正读取配置文件信息的类实际上是java.util.ResourceBundle.Control.Control对象,通过调用java.util.ResourceBundle.Control#newBundle()方法,加载配置文件信息。
2) java.util.ResourceBundle.Control.java
/*** 加载配置文件**/public ResourceBundle newBundle(String baseName, Locale locale, String format,ClassLoader loader, boolean reload)throws IllegalAccessException, InstantiationException, IOException{// 基于locale信息、baseName,生成bundleNameString bundleName = toBundleName(baseName, locale);ResourceBundle bundle = null;if (format.equals("java.class")) {......} else if (format.equals("java.properties")) {// 如果是包路径下的,这个可以将包路径变成文件路径// 比如com/zhru/wechat/jdk/international/myconfig.propertiesfinal 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<InputStream>() {public InputStream run() throws IOException {InputStream is = null;if (reloadFlag) {// 加载配置文件.......} else {// 加载配置文件is = classLoader.getResourceAsStream(resourceName);}return is;}});} catch (PrivilegedActionException e) {throw (IOException) e.getException();}if (stream != null) {try {// PropertyResourceBundle加载从配置文件中读取的字节流信息bundle = new PropertyResourceBundle(stream);} finally {stream.close();}}} else {throw new IllegalArgumentException("unknown format: " + format);}return bundle;}
代码跟到这里我们发现,读取类路径下的properties文件获取的字节流信息最终会被java.util.PropertyResourceBundle进行加载,进入这个类:
3)java.util.PropertyResourceBundle
依次跟踪PropertyResourceBundle的源代码
// java.util.PropertyResourceBundle/*** 从流信息{@link java.io.InputStream* InputStream}中创建一个PropertyResourceBundle.* 这个配置文件必须是ISO-8859-1的编码格式*/@SuppressWarnings({"unchecked", "rawtypes"})public PropertyResourceBundle(InputStream stream) throws IOException {// 实际调用的是java.util.Properties加载配置信息Properties properties = new Properties();properties.load(stream);lookup = new HashMap(properties);}/*** Creates a property resource bundle from a {@link java.io.Reader* Reader}. Unlike the constructor* {@link #PropertyResourceBundle(java.io.InputStream) PropertyResourceBundle(InputStream)},* there is no limitation as to the encoding of the input property file.** @param reader a Reader that represents a property file to* read from.* @throws IOException if an I/O error occurs* @throws NullPointerException if <code>reader</code> is null* @throws IllegalArgumentException if a malformed Unicode escape sequence appears* from {@code reader}.* @since 1.6*/public PropertyResourceBundle(Reader reader) throws IOException {Properties properties = new Properties();properties.load(reader);lookup = new HashMap(properties);}
从java.util.PropertyResourceBundle#PropertyResourceBundle(java.io.InputStream)的注释信息我们可以发现,虽然我们创建的properties文件的编码格式的UTF-8,但是PropertyResourceBundle在加载的时候,是以ISO-8859-1的格式进行读取,因此,必然会出现中文乱码的问题。
3. ResourceBundle加载中文配置乱码的解决方案
3.1 基于ResourceBundle.Control解决方案
3.1.1 基于ResourceBundle.Control解决方案的实现分析
从java.util.PropertyResourceBundle的源代码中我们可以发现,java.util.PropertyResourceBundle真正是调用的java.util.Properties类进行properties文件的读取,而java.util.Properties同时支持字节流(InputStream)和字符流(Reader)形式的加载,而字符流是可以手动的设置编码格式。
从上一小节分析的的源代码中,我们也可以清楚的看到,java.util.PropertyResourceBundle对象,不仅可以通过传递字节流创建,也可以通过传递字符流创建。
基于java.util.ResourceBundle代码的跟踪,我们知道java.util.ResourceBundle加载配置文件最终会调用java.util.ResourceBundle.Control#newBundle()方法,而java.util.ResourceBundle.Control对象是在java.util.ResourceBundle#getBundleImpl()方法调用的时候显示传递。因此,自然可以继承java.util.ResourceBundle.Control类,自定义java.util.ResourceBundle.Control,并在Control.newBundle()方法中将加载的配置文件信息以字符流的形式传递给java.util.PropertyResourceBundle中即可。
我们先来看下java.util.ResourceBundle重载的getBundle()方法
// java.util.ResourceBundle/*** 只需要baseName* @param baseName*/@CallerSensitivepublic static final ResourceBundle getBundle(String baseName){return getBundleImpl(baseName, Locale.getDefault(),getLoader(Reflection.getCallerClass()),getDefaultControl(baseName));}/*** 需要baseName和control* @param baseName* @param control*/@CallerSensitivepublic static final ResourceBundle getBundle(String baseName,Control control) {return getBundleImpl(baseName, Locale.getDefault(),getLoader(Reflection.getCallerClass()),control);}/*** 需要baseName和locale* @param baseName* @param locale*/@CallerSensitivepublic static final ResourceBundle getBundle(String baseName,Locale locale){return getBundleImpl(baseName, locale,getLoader(Reflection.getCallerClass()),getDefaultControl(baseName));}/*** 需要baseName、locale和control* @param baseName* @param locale* @param control*/@CallerSensitivepublic static final ResourceBundle getBundle(String baseName, Locale targetLocale,Control control) {return getBundleImpl(baseName, targetLocale,getLoader(Reflection.getCallerClass()),control);}/*** 需要baseName、locale和classLoader* @param baseName* @param locale* @param classLoader*/public static ResourceBundle getBundle(String baseName, Locale locale,ClassLoader loader){if (loader == null) {throw new NullPointerException();}return getBundleImpl(baseName, locale, loader, getDefaultControl(baseName));}/*** 需要baseName、locale、classLoader和control* @param baseName* @param locale* @param classLoader* @param control*/public static ResourceBundle getBundle(String baseName, Locale targetLocale,ClassLoader loader, Control control) {if (loader == null || control == null) {throw new NullPointerException();}return getBundleImpl(baseName, targetLocale, loader, control);}
从上面的代码中,我们可以得出这样的结论:当调用java.util.ResourceBundle#getBundle()方法时,如果提供了control对象则以提供的为准,否则用默认的ResourceBundle.Control对象。
3.1.2 基于ResourceBundle.Control解决方案的代码实现
可以模仿java.util.ResourceBundle.Control类,实现自定义的ResourceBundle.Control类,并复写它的newBundle()方法:
/*** Desp: 自定义 {@link ResourceBundle.Control}* 2019-10-27 14:31* Created by zhru.*/public class CustomControl extends ResourceBundle.Control {// properties文件读取时的编码格式private String encoding;public CustomControl() {this.encoding = "UTF-8";}public CustomControl(String encoding) {this.encoding = encoding;}@Overridepublic 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")) {......} 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 = ......} catch (PrivilegedActionException e) {throw (IOException) e.getException();}if (stream != null) {// 若encoding没有设置或者设置为null,则默认为UTF-8try {if (this.encoding == null) {this.encoding = "UTF-8";}// 以字符流的形式进行加载bundle = new PropertyResourceBundle(new InputStreamReader(stream, encoding));} finally {stream.close();}}} else {throw new IllegalArgumentException("unknown format: " + format);}return bundle;}private String toResourceName0(String bundleName, String suffix) {// application protocol checkif (bundleName.contains("://")) {return null;} else {return toResourceName(bundleName, suffix);}}}
核心的逻辑是在获取到properties文件的字节流信息后,将字节流转换成字符流,用于创建java.util.PropertyResourceBundle对象。
3.1.3 基于ResourceBundle.Control解决方案的测试
package com.zhru.wechat.jdk.international;import com.zhru.wechat.jdk.international.reslove.CustomControl;import java.util.Locale;import java.util.ResourceBundle;/*** Desp: 使用自定义的{@link java.util.ResourceBundle.Control}测试国际化操作* 2019-10-27 14:38* Created by zhru.*/public class ResourceBundleUseCustonControlDemo {public static void main(String[] args) {// 初始化Control对象ResourceBundle.Control control = new CustomControl("UTF-8");/*** 以en_US语言编码进行加载配置* 使用自定义的ResourceBundle.Control对象*/ResourceBundle bundle = ResourceBundle.getBundle("myconfig",Locale.US, control);String address = bundle.getString("address");System.out.println("当前的语言编码是:" + Locale.US+ ",获取的address值:" + address);/*** 以zh_CN语言编码进行加载配置* 使用自定义的ResourceBundle.Control对象*/bundle = ResourceBundle.getBundle("myconfig",Locale.SIMPLIFIED_CHINESE, control);address = bundle.getString("address");System.out.println("当前的语言编码是:" + Locale.SIMPLIFIED_CHINESE+ ",获取的address值:" + address);}}
使用自定义的ResourceBundle.Control对象测试,运行结果:
/usr/lib/jdk1.8.0_171/bin/java当前的语言编码是:en_US,获取的address值:BeiJing Province当前的语言编码是:zh_CN,获取的address值:北京市Process finished with exit code 0
从上面的运行结果可以看出来,已经解决了ResourceBundle加载配置文件中文乱码的问题,但是,上面的逻辑存在一个很严重的不足:每次调用ResourceBundle.getBundle()方法的时候,必须显示的传递自定义的ResourceBundle.Control对象,增大了编程的工作量,代码的移植性也比较低,是否有更好的方式去解决ResourceBundle加载中文乱码的问题呢,是否可以直接调用ResourceBundle.getBundle()方法的时候不传递ResourceBundle.Control对象呢?
既然我都这么说了,自然是有调用时更简便的方式^
3.2 基于java.util.spi.ResourceBundleControlProvider解决方案
备注:jdk版本需要在1.8以上
3.2.1 基于java.util.spi.ResourceBundleControlProvider解决方案分析
从 3.1.1 小节的代码中可以看出来,java.util.ResourceBundle重载了多个getBundle()方法,最简单的getBundle()方法如下面的代码所示:
// java.util.ResourceBundle/*** 只需要baseName* control对象由java.util.ResourceBundle#getDefaultControl()方法获取* @param baseName*/@CallerSensitivepublic static final ResourceBundle getBundle(String baseName){return getBundleImpl(baseName, Locale.getDefault(),getLoader(Reflection.getCallerClass()),getDefaultControl(baseName));}
从上面的代码,我们可以发现,当没有提供control对象的时候,实际调用的是java.util.ResourceBundle#getDefaultControl(String)方法获取control对象
// java.util.ResourceBundleprivate 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;}
遍历providers对象,那么providers对象是从哪里来的呢?我们继续看代码:
// java.util.ResourceBundleprivate static final List<ResourceBundleControlProvider> providers;static {List<ResourceBundleControlProvider> list = null;// 借助SPI,从ServiceLoader中加载ResourceBundleControlProviderServiceLoader<ResourceBundleControlProvider> serviceLoaders= ServiceLoader.loadInstalled(ResourceBundleControlProvider.class);// 遍历SPI获取的ResourceBundleControlProvider集合,// 添加到List<ResourceBundleControlProvider>集合for (ResourceBundleControlProvider provider : serviceLoaders) {if (list == null) {list = new ArrayList<>();}list.add(provider);}// 将List<ResourceBundleControlProvider>集合赋值给providersproviders = list;}
代码已经注释的很清楚,java.util.spi.ResourceBundleControlProvider是借助于JDK的SPI机制进行加载的(关于SPI机制,以后再讲,不了解的同学可以google学习)。
因此,借助SPI机制,只需要复写(继承) java.util.spi.ResourceBundleControlProvider类,然后在类路径下的META-INF/services目录下进行注册即可,这样java.util.ServiceLoader类就可以自动加载。
// java.util.ServiceLoader/*** 使用ExtClassLoader加载Service** 这个方法先找到ExtClassLoader,然后调用* ServiceLoader.load(service, extClassLoader)** 如果没有找到ExtClassLoader,那么使用System ClassLoader加载,* 如果没有找到 System ClassLoader,则使用Bootstrap ClassLoader** 这个方法只查找已经在JVM虚拟机中进行过装载的Service,应用程序* classpath路径下的将被忽略* @param <S> the class of the service type* @param service* The interface or abstract class representing the service* @return A new service loader*/public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {ClassLoader cl = ClassLoader.getSystemClassLoader();ClassLoader prev = null;while (cl != null) {prev = cl;cl = cl.getParent();}return ServiceLoader.load(service, prev);}
package com.zhru.wechat.jdk.international.reslove.spi;import com.zhru.wechat.jdk.international.reslove.control.CustomControl;import java.util.ResourceBundle;import java.util.spi.ResourceBundleControlProvider;/*** Desp: 自定义的{@link java.util.spi.ResourceBundleControlProvider}* 2019-10-27 16:44* Created by zhru.*/public class CustomResourceBundleControlProvider implements ResourceBundleControlProvider {@Overridepublic ResourceBundle.Control getControl(String baseName) {// file.encoding作为入参传递进来String encoding = System.getProperty("file.encoding");if (encoding == null) {encoding = "UTF-8";}return new CustomControl(encoding);}}
META-INF... services...... java.util.spi.ResourceBundleControlProvider
## java.util.spi.ResourceBundleControlProvider实现类com.zhru.wechat.jdk.international.reslove.spi.CustomResourceBundleControlProvider

/*** Desp: {@link java.util.ResourceBundle}加载配置信息* 2019-10-25 21:43* Created by zhru.*/public class ResourceBundleDemo {public static void main(String[] args) {/*** 以en_US语言编码进行加载配置*/ResourceBundle bundle = ResourceBundle.getBundle("myconfig", Locale.US);String address = bundle.getString("address");System.out.println("当前的语言编码是:" + Locale.US+ ",获取的address值:" + address);/*** 以zh_CN语言编码进行加载配置*/bundle = ResourceBundle.getBundle("myconfig", Locale.SIMPLIFIED_CHINESE);address = bundle.getString("address");System.out.println("当前的语言编码是:" + Locale.SIMPLIFIED_CHINESE+ ",获取的address值:" + address);}}

/usr/lib/jdk1.8.0_171/bin/java当前的语言编码是:en_US,获取的address值:BeiJing Province当前的语言编码是:zh_CN,获取的address值:北京市Process finished with exit code 0





