Java 提供的国际化支持
作者:互联网
参考资料
1. 缘由
以前学习 Java 的时候仅仅有这个概念,并没有深入了解。最近在开发 idea 插件,发现 CommonBundle 的父类 AbstractBundle 使用到了 java.util.ResourceBundle 。ResourceBundle 能够根据 Locale 从对应的消息文件(properties,xml)中读取消息。
1. 什么是国际化?
国际化(internationalization)是设计和制造容易适应不同区域要求的产品的一种方式,它要求从产品中抽离所有地域语言,国家/地区和文化相关的元素。换言之,应用程序的功能和代码设计考虑在不同地区运行的需要,其代码简化了不同本地版本的生产。开发这样的程序的过程,就称为国际化。
2. java.util.ResourceBundle
2.1 代码示例
- 新建 maven 项目,并在 src/main/resources 添加以下4个编码格式为 ISO-8859-1 的 properties 文件:
- message.properties
- message_en.properties
- message_zh_CN.properties
- message_zh_TW.properties
其内容分别是:
message.properties
welcome=这是默认欢迎信息!
message_en.properties
welcome=Welcome to my application !
message_zh_CN.properties
welcome=欢迎你来到我的应用 !
message_zh_TW.properties
welcome=歡迎來到我的應用!
- 编写 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;
}
}
- 编写获取消息类
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);
}
}
测试
- 编写测试类
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
欢迎标语: 歡迎來到我的應用!
-----------------------------------
局限性
- windos操作系统下 properties 文件编码必须是 ISO-8859-1,如果改为 utf-8 会产生乱码。
3. Spring 的国际化支持
3.1 MessageSource 消息源
Spring 提供了 org.springframework.context.MessageSource 接口,是解析消息的策略接口,支持消息的参数化和国际化。
Spring 还提供了2个开箱即用的实现类:
- org.springframework.context.support.ResourceBundleMessageSource(基于 java.util.ResourceBundle 构建的)
- org.springframework.context.support.ReloadableResourceBundleMessageSource(高度可配置化,可重新加载 message definitions)
ApplicationContext 接口继承了 MessageSource 接口,说明 Spring 容器具有消息参数化和国际化的能力。
接口继承结构:
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(实现 ResolveLocaleContext 接口)
- FixedLocaleResolver(实现 ResolveLocaleContext 接口)
- SessionLocaleResolver(实现 ResolveLocaleContext 接口)
- AcceptHeaderLocaleResolver
CookieLocaleResolver
检查 Cookie 是否有 TimeZone 和 Locale 信息。
FixedLocaleResolver
固定 TimeZone 和 Locale 信息。提供AbstractLocaleResolver#setDefaultLocale 和 AbstractLocaleContextResolver# setDefaultTimeZone
进行设定。
SessionLocaleResolver
从本次请求对应的 session 中检索 TimeZone 和 Locale 信息。
AcceptHeaderLocaleResolver
AcceptHeaderLocaleResolver 解析请求头的 accept-language 属性 获取Locale。
如下:
Locale 拦截器
使用 LocaleChangeInterceptor 可以对某个 HandlerMapping 改变其 Locale。
LocaleResolver 实现及继承结构图
3.3 总结
Spring 对国际化的支持:
Spring 使用 LocaleResolver 解析获取客户端的 Locale,MessageSource 使用此 Locale 决定从哪个文件中读取消息。
标签:国际化,Java,String,Locale,支持,locale,ResourceBundle,return,properties 来源: https://www.cnblogs.com/lihw-study/p/16327107.html