编程语言
首页 > 编程语言> > Java 提供的国际化支持

Java 提供的国际化支持

作者:互联网

目录

参考资料

  1. Intellij IDEA Resource Bundle

  2. Spring frame messagesource

  3. Spring MVC LocaleResolver

1. 缘由

以前学习 Java 的时候仅仅有这个概念,并没有深入了解。最近在开发 idea 插件,发现 CommonBundle 的父类 AbstractBundle 使用到了 java.util.ResourceBundle 。ResourceBundle 能够根据 Locale 从对应的消息文件(properties,xml)中读取消息。

1. 什么是国际化?

国际化(internationalization)是设计和制造容易适应不同区域要求的产品的一种方式,它要求从产品中抽离所有地域语言,国家/地区和文化相关的元素。换言之,应用程序的功能和代码设计考虑在不同地区运行的需要,其代码简化了不同本地版本的生产。开发这样的程序的过程,就称为国际化。

2. java.util.ResourceBundle

2.1 代码示例

  1. 新建 maven 项目,并在 src/main/resources 添加以下4个编码格式为 ISO-8859-1 的 properties 文件:

其内容分别是:
message.properties

welcome=这是默认欢迎信息!

message_en.properties

welcome=Welcome to my application !

message_zh_CN.properties

welcome=欢迎你来到我的应用 !

message_zh_TW.properties

welcome=歡迎來到我的應用!
  1. 编写 Locale 的工具类
import java.util.Locale;

/**
 * Locale  工具类
 */

public class LocaleUtil {

    /**
     * 根据 language tag 获取对应的 locale
     * @param languageTag  language tag (比如 zh-CN,zh,en 等)
     * @return locale
     */
    public static Locale localeByLanguageTag(String languageTag) {
        if(languageTag == null || languageTag.equals(" ") || languageTag.length() == 0) {
            return Locale.getDefault();
        }
        Locale locale;
        switch (languageTag.toLowerCase()) {
        case "zh":
            locale = Locale.CHINESE;
            break;
        case "zh-cn":
            locale = Locale.SIMPLIFIED_CHINESE;
            break;
        case "zh-tw":
            locale = Locale.TRADITIONAL_CHINESE;
            break;
        case "en":
            locale = Locale.ENGLISH;
            break;
        default:
            locale = Locale.ROOT;
            break;
        }
        return locale;
    }
}
  1. 编写获取消息类
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * 该类主要用于 properties文件获取信息
 * @author black
 *
 */
public class MessageBundle {

    // 国际化文件名字
    private static final String MESSAGE_FILE_NAME="message";
    // JDK 提供 ResourceBundle 类用于支持国际化
    private ResourceBundle bundle;

    public MessageBundle(String languageTag) {
        // 1. 获取 locale
        Locale locale = LocaleUtil.localeByLanguageTag(languageTag);
        // 2. 实例化 ResourceBundle,即告诉 ResourceBundle 从哪个properties文件获取信息
        bundle = ResourceBundle.getBundle(MESSAGE_FILE_NAME, locale);
    }

    /**
     * 获取消息
     * @param key properties文件里的消息key
     * @return
     */
    public String getMessage(String key) {
        return bundle.getString(key);
    }
}

测试

  1. 编写测试类

import java.util.Scanner;

/**
 * 测试类
 * @author black
 *
 */
public class MessageBundleTest {

    public static void main(String[] args) {
        while (true) {
            System.out.println("-----------------------------------");
            // 1. 扫描输入的 language tag
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入Accept-Language:");
            String languageTag = scanner.next();
            // 2. 根据 language tag 获取对应的【欢迎信息】
            String welcome = getMessage(languageTag, "welcome");
            System.out.println("欢迎标语: " + welcome);
            System.out.println("-----------------------------------");
        }

    }

    private static String getMessage(String languageTag, String key) {
        MessageBundle messageBundle = new MessageBundle(languageTag);
        return messageBundle.getMessage(key);
    }
}

运行测试,控制台输出:

-----------------------------------
请输入Accept-Language:default
欢迎标语: 这是默认欢迎信息!
-----------------------------------
-----------------------------------
请输入Accept-Language:en
欢迎标语: Welcome to my application !
-----------------------------------
-----------------------------------
请输入Accept-Language:zh-CN
欢迎标语: 欢迎你来到我的应用 !
-----------------------------------
-----------------------------------
请输入Accept-Language:zh-TW
欢迎标语: 歡迎來到我的應用!
-----------------------------------

局限性

  1. windos操作系统下 properties 文件编码必须是 ISO-8859-1,如果改为 utf-8 会产生乱码。

3. Spring 的国际化支持

3.1 MessageSource 消息源

Spring 提供了 org.springframework.context.MessageSource 接口,是解析消息的策略接口,支持消息的参数化和国际化。

Spring 还提供了2个开箱即用的实现类:

ApplicationContext 接口继承了 MessageSource 接口,说明 Spring 容器具有消息参数化和国际化的能力。

接口继承结构:
image

3.1.1 ResourceBundleMessageSource

ResourceBundleMessageSource 依赖于 java.util.ResourceBundle,结合JDK java.text.MessageFormat 提供的标准消息解析。

由于java.util.ResourceBundle 则是根据 Locale 读取某个 properties 文件,所以 ResourceBundleMessageSource 的信息源就是国际化消息 properties 文件。

那么 ResourceBundleMessageSource 怎么知道读取哪些 properties 文件呢?

答:其父类 AbstractResourceBasedMessageSource 的 setBasenames(String... basenames) 设置的 (原理是通过 Spring 容器的 Setter 注入实现的)

<beans>
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

上面 xml 配置的含义是 ResourceBundleMessageSource 将会从 format.properties("format_en.properties",format.xml", "format_en.xml") ,exceptions.properties("exceptions_en.properties",exceptions.xml", "exceptions_en.xml") 和 windows.properties("windows_en.properties",windows.xml", "windows_en.xml") 文件中读取 message。

那么 ResourceBundleMessageSource 怎么解决 properties 文件的编码呢?
答:对 ResourceBundle.Control 提供了定制化实现,即 MessageSourceControl 类。MessageSourceControl 类代码:

private volatile MessageSourceControl control = new MessageSourceControl();
	/**
	 * Custom implementation of {@code ResourceBundle.Control}, adding support
	 * for custom file encodings, deactivating the fallback to the system locale
	 * and activating ResourceBundle's native cache, if desired.
	 */
	private class MessageSourceControl extends ResourceBundle.Control {

		@Override
		@Nullable
		public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
				throws IllegalAccessException, InstantiationException, IOException {

			// Special handling of default encoding
			if (format.equals("java.properties")) {
				String bundleName = toBundleName(baseName, locale);
				final String resourceName = toResourceName(bundleName, "properties");
				final ClassLoader classLoader = loader;
				final boolean reloadFlag = reload;
				InputStream inputStream;
				try {
					inputStream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
						InputStream is = null;
						if (reloadFlag) {
							URL url = classLoader.getResource(resourceName);
							if (url != null) {
								URLConnection connection = url.openConnection();
								if (connection != null) {
									connection.setUseCaches(false);
									is = connection.getInputStream();
								}
							}
						}
						else {
							is = classLoader.getResourceAsStream(resourceName);
						}
						return is;
					});
				}
				catch (PrivilegedActionException ex) {
					throw (IOException) ex.getException();
				}
				if (inputStream != null) {
					String encoding = getDefaultEncoding();
					if (encoding != null) {
						try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) {
							return loadBundle(bundleReader);
						}
					}
					else {
						try (InputStream bundleStream = inputStream) {
							return loadBundle(bundleStream);
						}
					}
				}
				else {
					return null;
				}
			}
			else {
				// Delegate handling of "java.class" format to standard Control
				return super.newBundle(baseName, locale, format, loader, reload);
			}
		}

		@Override
		@Nullable
		public Locale getFallbackLocale(String baseName, Locale locale) {
			Locale defaultLocale = getDefaultLocale();
			return (defaultLocale != null && !defaultLocale.equals(locale) ? defaultLocale : null);
		}

		@Override
		public long getTimeToLive(String baseName, Locale locale) {
			long cacheMillis = getCacheMillis();
			return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale));
		}

		@Override
		public boolean needsReload(
				String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {

			if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) {
				cachedBundleMessageFormats.remove(bundle);
				return true;
			}
			else {
				return false;
			}
		}
	}

其中:

// encoding 可通过 AbstractResourceBasedMessageSource#setDefaultEncoding 方法修改文件编码
String encoding = getDefaultEncoding();
if (encoding != null) {
   // 使用指定文件编码,读取文件流,并构建
	try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) {
 	// 返回 new PropertyResourceBundle(inputStream);
	return loadBundle(bundleReader);
	}
}

3.1.2 ReloadableResourceBundleMessageSource

ReloadableResourceBundleMessageSource 使用 Properties 保存从文件中读取到的消息,支持消息的重新加载。

根据 basename + Locale 判断读取哪个文件。

文件编码可使用 ReloadableResourceBundleMessageSource#setFileEncodings 指定:

/**
 * fileEncodings 内容:文件名=文件编码
 * 针对不同的文件灵活配置其编码
 */
public void setFileEncodings(Properties fileEncodings) {
		this.fileEncodings = fileEncodings;
	}

当然,如果没有设置 fileEncodings,那么可以通过 AbstractResourceBasedMessageSource#setDefaultEncoding 指定默认编码。

3.2 LocaleResolver 解析获取 Locale

DispatcherServlet 允许通过客户端的 Locale对象 (Locale对象表示特定的地理、政治或文化区域) 自动处理消息。这个过程由 LocaleResolver 对象完成的。

Locale 处理器和拦截器都位于 org.springframework.web.servlet.i18n 包中。

LocaleContextResolver 接口提供了 resolveLocaleContext 方法可解析获取 Locale 和 TimeZone 信息。

Spring 提供的 locale resolvers 如下:

CookieLocaleResolver
检查 Cookie 是否有 TimeZone 和 Locale 信息。

FixedLocaleResolver
固定 TimeZone 和 Locale 信息。提供AbstractLocaleResolver#setDefaultLocale 和 AbstractLocaleContextResolver# setDefaultTimeZone
进行设定。

SessionLocaleResolver
从本次请求对应的 session 中检索 TimeZone 和 Locale 信息。

AcceptHeaderLocaleResolver
AcceptHeaderLocaleResolver 解析请求头的 accept-language 属性 获取Locale。
如下:
image
Locale 拦截器
使用 LocaleChangeInterceptor 可以对某个 HandlerMapping 改变其 Locale。

LocaleResolver 实现及继承结构图
image

3.3 总结

Spring 对国际化的支持:

Spring 使用 LocaleResolver 解析获取客户端的 Locale,MessageSource 使用此 Locale 决定从哪个文件中读取消息。

标签:国际化,Java,String,Locale,支持,locale,ResourceBundle,return,properties
来源: https://www.cnblogs.com/lihw-study/p/16327107.html