不安分的黑娃
踏踏实实,坚持学习,慢慢就懂了~

参考资料

  1. Intellij IDEA Resource Bundle

  2. Spring frame messagesource

  3. Spring MVC LocaleResolver

  4. https://docs.oracle.com/javase/8/docs/technotes/guides/intl/overview.html

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
  • 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=歡迎來到我的應用!
  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个开箱即用的实现类:

  • org.springframework.context.support.ResourceBundleMessageSource(基于 java.util.ResourceBundle 构建的)
  • org.springframework.context.support.ReloadableResourceBundleMessageSource(高度可配置化,可重新加载 message definitions)

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(实现 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。
如下:
image
Locale 拦截器
使用 LocaleChangeInterceptor 可以对某个 HandlerMapping 改变其 Locale。

LocaleResolver 实现及继承结构图
image

3.3 总结

Spring 对国际化的支持:

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

posted on 2022-05-31 11:24  不安分的黑娃  阅读(609)  评论(0编辑  收藏  举报