Java原生国际化

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 bundleClass
                        = (Class)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 bundleClass
                        = (Class) 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国际化消息解析原理

你可能感兴趣的:(Java原生国际化)