Java原生国际化
文档地址
自定义国际化案例
public class LocaleDemo {
public static void main(String[] args) {
System.out.println(Locale.getDefault());
}
}
获取本地方言
配置本地方言
-
通过启动参数-D命令配置
- 但是这种方式只适合本机
Locale.setDefault(Locale.US);
国际化数字
public class NumberFormatDemo {
public static void main(String[] args) {
NumberFormat numberFormat = NumberFormat.getNumberInstance();
System.out.println(numberFormat.format(10000));//10,000
numberFormat = NumberFormat.getNumberInstance(Locale.FRANCE);
System.out.println(numberFormat.format(10000));//10 000
}
}
通过不同的方言来决定数字的显示方式
ResourceBundle国际化
创建一个demo_zh_CN.properties
在resources目录
name=测试
world=你好,{0}
public class ResourceBundleDemo {
public static final String BUNDLE_NAME = "demo";
public static void main(String[] args) {
getEn();
getZhCn();
}
private static void getZhCn() {
Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
ResourceBundle demo2 = ResourceBundle.getBundle(BUNDLE_NAME);
//因为当前没有使用unicode来写,默认是iso_8859_1,所以转化,避免乱码
System.out.println(new String(demo2.getString("name").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
}
private static void getEn() {
Locale.setDefault(Locale.ENGLISH);
ResourceBundle demo = ResourceBundle.getBundle(BUNDLE_NAME);
String test = demo.getString("name");
System.out.println(test);
}
}
上述代码中通过java.util.ResourceBundle
来做国际化转化,但是因为properties文件中的国际化内容默认采用的是ISO 8895-1
所以只要出现的是中文就会乱码。当前我们使用的是通过字符串编解码来转化的。
国际化乱码问题
从上述案例中我们可以看到中文会乱码。
解决方式有以下三种:
- 可以采用
jdk
自带的工具 native2ascii 方法,将打包后的资源文件进行转移,而不是直接在源码方面解决 -
扩展 ResourceBundle.Control
- 缺点:可移植性不强,不得不显示地传递
- 实现 ResourceBundleControlProvider
jdk自带的native2ascii
native2ascii demo_zh_CN.properties demo_zh_CN_ascii.properties
转化后文件内容如下
name=\u6d4b\u8bd5
world=\u4f60\u597d,{0}
扩展java.util.ResourceBundle.Control
从java.util.ResourceBundle.Control#newBundle
可以看到java.util.ResourceBundle
是从这里生产出来的。
核心代码如下
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 {
//把读取到的流装载到PropertyResourceBundle中
bundle = new PropertyResourceBundle(stream);
} finally {
stream.close();
}
}
java.util.PropertyResourceBundle#PropertyResourceBundle(java.io.InputStream)
public PropertyResourceBundle (InputStream stream) throws IOException {
Properties properties = new Properties();
properties.load(stream);
lookup = new HashMap(properties);
}
断点查看,在Peroerties加载stream的时候出现了乱码。
所以我们可以在获取到流的时候,直接定义流的编码就行了
所以照葫芦画瓢,修改代码如下
public class EncodedControl extends ResourceBundle.Control {
private final String encoding;
public EncodedControl(String encoding) {
this.encoding = encoding;
}
@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 extends ResourceBundle> bundleClass
= (Class extends ResourceBundle>) 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() {
@Override
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();
}
Reader reader = null;
if (stream != null) {
try {
//增加转码
reader = new InputStreamReader(stream, encoding);
bundle = new PropertyResourceBundle(reader);
} finally {
reader.close();
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);
}
}
}
修改代码
/**
* 基于 Java 1.6
* 显示地传递 EncodedControl
*/
private static void extendControl() {
Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
ResourceBundle resourceBundle = ResourceBundle.getBundle(BUNDLE_NAME, new EncodedControl("utf8"));
System.out.println("resourceBundle.name : " + resourceBundle.getString("name"));
}
测试,发现成功了。
但是这种方式可移植性不强,不得不显示地传递 ResourceBundle.Control
所以我们采用下面这种方式
实现ResourceBundleControlProvider
在
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;
}
这里可以看到,当我们ResourceBundle初始化的时候会基于SPI自动加载provider,在java.util.ResourceBundle#getDefaultControl
这里可以看到
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.ResourceBundleControlProvider
中获取,所以我们可以自定义java.util.spi.ResourceBundleControlProvider
来生成对应的control
SPI
spi原理具体见java.util.ServiceLoader.LazyIterator#hasNextService
private static final String PREFIX = "META-INF/services/";
编写代码
public class EncodingResourceBundleControlProvider implements ResourceBundleControlProvider {
@Override
public ResourceBundle.Control getControl(String baseName) {
return new EncodedControl();
}
}
然后按照文档
在META-INF/services
创建java.util.spi.ResourceBundleControlProvider
文件
内容为
com.zzjson.se.provider.EncodingResourceBundleControlProvider
最后测试
但是发现失效!!!
原因resourceBundle中spi调用的是java.util.ServiceLoader#loadInstalled
这里面不会加载项目中的配置
Spring国际化
MessageSource-消息转化的顶层接口
public interface MessageSource {
//用于从MessageSource检索消息的基本方法。 如果找不到指定语言环境的消息,则使用默认消息。 使用标准库提供的MessageFormat功能,传入的所有参数都将成为替换值。
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
//与先前的方法基本相同,但有一个区别:无法指定默认消息;默认值为0。 如果找不到消息,则抛出NoSuchMessageException。
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
//前述方法中使用的所有属性也都包装在一个名为MessageSourceResolvable的类中,您可以在此方法中使用该类。
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
加载ApplicationContext时,它将自动搜索在上下文中定义的MessageSource bean。
Bean必须具有名称messageSource。 如果找到了这样的bean,则对先前方法的所有调用都将委派给消息源。
如果找不到消息源,则ApplicationContext尝试查找包含同名bean的父级。 如果是这样,它将使用该bean作为MessageSource。
如果ApplicationContext找不到任何消息源,则将实例化一个空的org.springframework.context.support.DelegatingMessageSource
,以便能够接受对上述方法的调用。
MessageSourceResolvable
org.springframework.context.MessageSourceResolvable
public interface MessageSourceResolvable {
String[] getCodes();
Object[] getArguments();
String getDefaultMessage();
}
类图
当前我们只需要关注这一块就行了
HierarchicalMessageSource
public interface HierarchicalMessageSource extends MessageSource {
void setParentMessageSource(MessageSource parent);
MessageSource getParentMessageSource();
}
MessageSourceSupport
MessageFormat
MessageFormat是java提供的他的包在java.text
,他能帮我们格式化文本
MessageSourceSupport和MessageFormat密切相关我们先看看MessageFormat的案例
public class MessageFormatDemo {
/**
* @param args
* @see ResourceBundleMessageSource#resolveCode(java.lang.String, java.util.Locale)
*/
public static void main(String[] args) {
MessageFormat format = new MessageFormat("Hello,{0}!");
System.out.println(format.format(new Object[]{"World"}));
}
}
- MessageFormat能够帮我们填充参数
java.text.MessageFormat#subformat
回到org.springframework.context.support.MessageSourceSupport
可以看到其提供了标准的java.text.MessageFormat
功能查看其核心代码
public abstract class MessageSourceSupport {
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
private boolean alwaysUseMessageFormat = false;
private final Map> messageFormatsPerMessage =
new HashMap>();
//使用缓存的MessageFormats格式化给定的消息字符串。默认情况下,将为传入的默认消息调用,以解析在其中找到的所有参数占位符。
protected String formatMessage(String msg, Object[] args, Locale locale) {
if (msg == null || (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args))) {
return msg;
}
MessageFormat messageFormat = null;
synchronized (this.messageFormatsPerMessage) {
Map messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
if (messageFormatsPerLocale != null) {
messageFormat = messageFormatsPerLocale.get(locale);
}
else {
messageFormatsPerLocale = new HashMap();
this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);
}
if (messageFormat == null) {
try {
messageFormat = createMessageFormat(msg, locale);
}
catch (IllegalArgumentException ex) {
// Invalid message format - probably not intended for formatting,
// rather using a message structure with no arguments involved...
if (isAlwaysUseMessageFormat()) {
throw ex;
}
// Silently proceed with raw message if format not enforced...
messageFormat = INVALID_MESSAGE_FORMAT;
}
messageFormatsPerLocale.put(locale, messageFormat);
}
}
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
synchronized (messageFormat) {
return messageFormat.format(resolveArguments(args, locale));
}
}
//为给定的消息和语言环境创建一个MessageFormat。
protected MessageFormat createMessageFormat(String msg, Locale locale) {
return new MessageFormat((msg != null ? msg : ""), locale);
}
}
从代码中可见org.springframework.context.support.MessageSourceSupport
主要提供了一下几个功能
- 使用创建对应的Messageformat
- 缓存语言环境和对应的MessageFormat
AbstractMessageSource模板类
org.springframework.context.support.AbstractMessageSource
实现消息的通用处理,从而可以轻松地针对具体的MessageSource实施特定策略。
先看AbstractMessageSource
对于MessageSource
的默认实现
@Override
public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
if (defaultMessage == null) {
String fallback = getDefaultMessage(code);
if (fallback != null) {
return fallback;
}
}
return renderDefaultMessage(defaultMessage, args, locale);
}
@Override
public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale);
if (msg != null) {
return msg;
}
String fallback = getDefaultMessage(code);
if (fallback != null) {
return fallback;
}
throw new NoSuchMessageException(code, locale);
}
@Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
String[] codes = resolvable.getCodes();
if (codes != null) {
for (String code : codes) {
String message = getMessageInternal(code, resolvable.getArguments(), locale);
if (message != null) {
return message;
}
}
}
String defaultMessage = getDefaultMessage(resolvable, locale);
if (defaultMessage != null) {
return defaultMessage;
}
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : null, locale);
}
结合前面说的MessageSource接口的定义我们不难看出这里有两个核心的方法
-
org.springframework.context.support.AbstractMessageSource#getMessageInternal
- 在给定的语言环境中将给定的代码和参数解析为消息
-
org.springframework.context.support.AbstractMessageSource#getDefaultMessage(org.springframework.context.MessageSourceResolvable, java.util.Locale)
- 如果上述解析出来的Message是空的,则通过此方法获取默认消息
getDefaultMessage
protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) {
String defaultMessage = resolvable.getDefaultMessage();
String[] codes = resolvable.getCodes();
if (defaultMessage != null) {
if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) {
// Never format a code-as-default-message, even with alwaysUseMessageFormat=true
return defaultMessage;
}
//调用前面说到的`org.springframework.context.support.MessageSourceSupport#renderDefaultMessage`
return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale);
}
return (!ObjectUtils.isEmpty(codes) ? getDefaultMessage(codes[0]) : null);
}
从这里可以看到就是把参数传递给了我们前面说的MessageSourceSupport
中的方法然后对传入的参数基于语言环境进行了格式化
getMessageInternal
org.springframework.context.support.AbstractMessageSource#getMessageInternal
protected String getMessageInternal(String code, Object[] args, Locale locale) {
if (code == null) {
return null;
}
if (locale == null) {
locale = Locale.getDefault();
}
Object[] argsToUse = args;
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
// 当前代码可能需要优化,因为我们并不需要参数因此不需要涉及MessageFormat。但是实际上还是使用了MessageFormat去格式化消息
//注意,默认实现仍使用MessageFormat;
//这可以在特定的子类中覆盖
String message = resolveCodeWithoutArguments(code, locale);
if (message != null) {
return message;
}
}
else {
//对于在父MessageSource中定义了消息
//而在子MessageSource中定义了可解析参数的情况,直接子MessageSource就解析参数。
//把需要解析的参数封装到数组中
argsToUse = resolveArguments(args, locale);
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
//使用消息格式化器来格式
return messageFormat.format(argsToUse);
}
}
}
//如果上面都没有找到合适的解析器,即子类没有返回MessageFormat,则从语言环境无关的公共消息中的给定消息代码
//private Properties commonMessages;
// 当前commonMessage就是Properties
Properties commonMessages = getCommonMessages();
if (commonMessages != null) {
String commonMessage = commonMessages.getProperty(code);
if (commonMessage != null) {
return formatMessage(commonMessage, args, locale);
}
}
//如果都没有找到,就从父节点找
return getMessageFromParent(code, argsToUse, locale);
}
@Override
//把需要解析的参数封装到数组中
protected Object[] resolveArguments(Object[] args, Locale locale) {
if (args == null) {
return new Object[0];
}
List
上述从代码中可以看出来模板类主要做了以下几件事情和提出了一个未来版本或者子类重写需要优化的地方
-
提供了模板方法解析消息
- 对于没有args的并且参数为空的直接交给子类重写的
org.springframework.context.support.AbstractMessageSource#resolveCodeWithoutArguments
去解析 - 其他的则先
org.springframework.context.support.AbstractMessageSource#resolveArguments
把参数变成参数数组,然后调用子类的org.springframework.context.support.AbstractMessageSource#resolveCode
获取到MessageFormat - 如果都没有返回对应的MessageFormat则直接从
Properties
中获取 - 最后如果当前层还是没有获取到,则利用
org.springframework.context.HierarchicalMessageSource
来递归调用父类的org.springframework.context.support.AbstractMessageSource#getMessageInternal
- 对于没有args的并且参数为空的直接交给子类重写的
查看子类可以看到其有三个子类
ResourceBundleMessageSource
当前类是基于JDK的java.util.ResourceBundle
来实现的
查看org.springframework.context.support.ResourceBundleMessageSource.MessageSourceControl
可以看到,其自定义了一个Control来解析国际化,以及增加了编解码的功能,为了解决国际化乱码的问题
if (stream != null) {
String encoding = getDefaultEncoding();
if (encoding == null) {
encoding = "ISO-8859-1";
}
try {
return loadBundle(new InputStreamReader(stream, encoding));
}
finally {
stream.close();
}
}
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
Set basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
String result = getStringOrNull(bundle, code);
if (result != null) {
return result;
}
}
}
return null;
}
/**
* Resolves the given message code as key in the registered resource bundles,
* using a cached MessageFormat instance per message code.
*/
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
Set basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
MessageFormat messageFormat = getMessageFormat(bundle, code, locale);
if (messageFormat != null) {
return messageFormat;
}
}
}
return null;
}
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
if (getCacheMillis() >= 0) {
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
// do its native caching, at the expense of more extensive lookup steps.
return doGetBundle(basename, locale);
}
else {
// Cache forever: prefer locale cache over repeated getBundle calls.
synchronized (this.cachedResourceBundles) {
Map localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
return bundle;
}
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = new HashMap();
this.cachedResourceBundles.put(basename, localeMap);
}
localeMap.put(locale, bundle);
return bundle;
}
catch (MissingResourceException ex) {
if (logger.isWarnEnabled()) {
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
}
// Assume bundle not found
// -> do NOT throw the exception to allow for checking parent message source.
return null;
}
}
}
}
查看上述代码org.springframework.context.support.ResourceBundleMessageSource#resolveCodeWithoutArguments
可知其从basenames位置获取了国际化信息,拿到了结果
org.springframework.context.support.ResourceBundleMessageSource#resolveCode
中可以见到返回了java.text.MessageFormat
并且设置了国际化信息
org.springframework.context.support.ResourceBundleMessageSource#getResourceBundle
中做了几件事情
- 如果指定了了本地缓存的时间则会超时后重新获取
- 如果没有指定本地缓存时间则直接都存储在了
org.springframework.context.support.ResourceBundleMessageSource#cachedResourceBundles
中
当前类缺点也是很明显,只能从类路径读取,不能指定外部文件
ReloadableResourceBundleMessageSource
当前类支持相同的包文件格式,但比基于标准JDK的ResourceBundleMessageSource
实现更灵活。
特别是,它允许从任何Spring资源位置读取文件(不仅仅是从类路径),并支持热重载bundle属性文件(同时在两者之间有效地缓存它们)。
默认的重载方法
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
else {
for (String basename : getBasenameSet()) {
List filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
}
return null;
}
/**
* Resolves the given message code as key in the retrieved bundle files,
* using a cached MessageFormat instance per message code.
*/
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
else {
for (String basename : getBasenameSet()) {
List filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
}
return null;
}
protected PropertiesHolder getMergedProperties(Locale locale) {
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
if (mergedHolder != null) {
return mergedHolder;
}
Properties mergedProps = newProperties();
long latestTimestamp = -1;
String[] basenames = StringUtils.toStringArray(getBasenameSet());
for (int i = basenames.length - 1; i >= 0; i--) {
List filenames = calculateAllFilenames(basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() != null) {
mergedProps.putAll(propHolder.getProperties());
if (propHolder.getFileTimestamp() > latestTimestamp) {
latestTimestamp = propHolder.getFileTimestamp();
}
}
}
}
mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp);
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
protected List calculateAllFilenames(String basename, Locale locale) {
Map> localeMap = this.cachedFilenames.get(basename);
if (localeMap != null) {
List filenames = localeMap.get(locale);
if (filenames != null) {
return filenames;
}
}
List filenames = new ArrayList(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
if (isFallbackToSystemLocale() && !locale.equals(Locale.getDefault())) {
List fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
for (String fallbackFilename : fallbackFilenames) {
if (!filenames.contains(fallbackFilename)) {
// Entry for fallback locale that isn't already in filenames list.
filenames.add(fallbackFilename);
}
}
}
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap>();
Map> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
return filenames;
}
//计算给定包基本名称和语言环境的文件名
protected List calculateFilenamesForLocale(String basename, Locale locale) {
List result = new ArrayList(3);
String language = locale.getLanguage();
String country = locale.getCountry();
String variant = locale.getVariant();
StringBuilder temp = new StringBuilder(basename);
temp.append('_');
if (language.length() > 0) {
temp.append(language);
result.add(0, temp.toString());
}
temp.append('_');
if (country.length() > 0) {
temp.append(country);
result.add(0, temp.toString());
}
if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) {
temp.append('_').append(variant);
result.add(0, temp.toString());
}
return result;
}
//
protected PropertiesHolder getMergedProperties(Locale locale) {
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
if (mergedHolder != null) {
return mergedHolder;
}
Properties mergedProps = newProperties();
long latestTimestamp = -1;
String[] basenames = StringUtils.toStringArray(getBasenameSet());
for (int i = basenames.length - 1; i >= 0; i--) {
List filenames = calculateAllFilenames(basenames[i], locale);
for (int j = filenames.size() - 1; j >= 0; j--) {
String filename = filenames.get(j);
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() != null) {
mergedProps.putAll(propHolder.getProperties());
if (propHolder.getFileTimestamp() > latestTimestamp) {
latestTimestamp = propHolder.getFileTimestamp();
}
}
}
}
mergedHolder = new PropertiesHolder(mergedProps, latestTimestamp);
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
ReloadableResourceBundleMessageSource.PropertiesHolder
用于缓存。
核心加载配置的代码
protected Properties loadProperties(Resource resource, String filename) throws IOException {
InputStream is = resource.getInputStream();
Properties props = newProperties();
try {
if (resource.getFilename().endsWith(XML_SUFFIX)) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.loadFromXml(props, is);
}
else {
String encoding = null;
if (this.fileEncodings != null) {
encoding = this.fileEncodings.getProperty(filename);
}
if (encoding == null) {
encoding = getDefaultEncoding();
}
if (encoding != null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'");
}
this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
}
this.propertiesPersister.load(props, is);
}
}
return props;
}
finally {
is.close();
}
}
org.springframework.context.support.ReloadableResourceBundleMessageSource#loadProperties
这里通过org.springframework.context.support.ReloadableResourceBundleMessageSource#calculateAllFilenames
以及org.springframework.context.support.ReloadableResourceBundleMessageSource#calculateFilenamesForLocale
计算出来的对应方言的路径加载到properties中,然后把获取到的properties
放到org.springframework.context.support.ReloadableResourceBundleMessageSource.PropertiesHolder
中持有,当前类会存储源文件的最后修改的时间戳,然后判断最后修改的时间戳和当前时间差值比较,判断是否超过了允许的最大缓存时间。
使用
public class SpringI18nDemo {
public static final String BUNDLE_NAME = "demo";
public static void main(String[] args) {
// ResourceBundle + MessageFormat => MessageSource
// ResourceBundleMessageSource 不能重载
// ReloadableResourceBundleMessageSource
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding("utf-8");
messageSource.setBasename(BUNDLE_NAME);
String name = messageSource
.getMessage("world", new Object[]{"World"}, Locale.SIMPLIFIED_CHINESE);
System.out.println(name);
}
}
StaticMessageSource
StaticMessageSource很少使用,相比之下就比较简单了
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
return this.messages.get(code + '_' + locale.toString());
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
String key = code + '_' + locale.toString();
String msg = this.messages.get(key);
if (msg == null) {
return null;
}
synchronized (this.cachedMessageFormats) {
MessageFormat messageFormat = this.cachedMessageFormats.get(key);
if (messageFormat == null) {
messageFormat = createMessageFormat(msg, locale);
this.cachedMessageFormats.put(key, messageFormat);
}
return messageFormat;
}
}
只是很简单的从静态map中获取值
常用api
Locale存储器-LocaleContext
public interface LocaleContext {
/**
* Return the current Locale, which can be fixed or determined dynamically,
* depending on the implementation strategy.
* @return the current Locale, or {@code null} if no specific Locale associated
*/
Locale getLocale();
}
public interface TimeZoneAwareLocaleContext extends LocaleContext {
/**
* Return the current TimeZone, which can be fixed or determined dynamically,
* depending on the implementation strategy.
* @return the current TimeZone, or {@code null} if no specific TimeZone associated
*/
TimeZone getTimeZone();
}
查看上述可知TimeZoneAwareLocaleContext
增加了时区的概念。
像这种存储器大部分都是写的关于TimeZoneAwareLocaleContext
的匿名类
例如org.springframework.web.servlet.i18n.FixedLocaleResolver#resolveLocaleContext
@Override
public LocaleContext resolveLocaleContext(HttpServletRequest request) {
return new TimeZoneAwareLocaleContext() {
@Override
public Locale getLocale() {
return getDefaultLocale();
}
@Override
public TimeZone getTimeZone() {
return getDefaultTimeZone();
}
};
}
Locale线程关联器-LocaleContextHolder
可以通过LocaleContextHolder类将LocaleContext实例与线程关联。
org.springframework.context.i18n.LocaleContextHolder
Locale解析器-LocaleResolver
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}
我们可以使用客户端的语言环境自动解析器org.springframework.web.servlet.LocaleResolver
来自动解析消息。
如上图所述Spring提供了几个获取国际化信息的解析器:
org.springframework.web.servlet.i18n.SessionLocaleResolver
org.springframework.web.servlet.i18n.CookieLocaleResolver
-
org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
- 此解析器检查客户端(例如,web浏览器)发送的请求中的accept-language头。
- 通常这个头字段包含客户操作系统的语言环境。
请注意,此解析器不支持时区信息
org.springframework.web.servlet.i18n.FixedLocaleResolver
CookieLocaleResolver
org.springframework.web.servlet.i18n.CookieLocaleResolver
此区域设置解析器检查客户端上可能存在的Cookie,以查看是否指定了区域设置或时区。如果是,则使用指定的详细信息。使用此区域设置解析器的属性,可以指定cookie的名称以及存活时间。下面是定义CookieLocaleResolver的一个示例。
SessionLocaleResolver
org.springframework.web.servlet.i18n.SessionLocaleResolver
SessionLocaleResolver允许我们从可能与用户请求关联的会话中检索Locale和TimeZone。
与CookieLocaleResolver相比,此策略将本地选择的语言环境设置存储在Servlet容器的HttpSession中。
因此,这些设置对于每个会话来说都是临时的,因此在每个会话终止时都会丢失。请注意,与外部会话管理机制(如Spring Session项目)没有直接关系。
该SessionLocaleResolver将仅根据当前的HttpServletRequest评估并修改相应的HttpSession属性。
FixedLocaleResolver
org.springframework.web.servlet.i18n.FixedLocaleResolver
指定固定的方言和时区,不允许修改修改会报错
@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
}
获取Locale
当我们收到请求时,DispatcherServlet会查找语言环境解析器,如果找到了它,则尝试使用它来设置语言环境。 使 用RequestContext.getLocale
方法,您始终可以检索由语言环境解析器解析的语言环境。
语言环境解析器和拦截器在org.springframework.web.servlet.i18n
包中定义,并以常规方式在应用程序上下文中进行配置。 这是Spring中包含的语言环境解析器的一部分。
org.springframework.web.servlet.support.RequestContext#getLocale
获取时区信息
LocaleContextResolver接口提供了LocaleResolver的扩展,该扩展允许解析程序提供更丰富的LocaleContext,其中可能包含时区信息。
如果可用,则可以使用RequestContext.getTimeZone()
方法获取用户的TimeZone。
在Spring的ConversionService中注册的日期/时间转换器和格式化程序对象将自动使用时区信息。
更改国际化
我们除了自动的语言环境解析之外,您还可以在处理程序映射上附加拦截器LocaleChangeInterceptor
以在特定情况下更改语言环境。
LocaleChangeInterceptor
我们能够很方便的更改国际化,通过参数来更改我们的国际化内容,通过增加一个LocaleChangeInterceptor
拦截器给一个handler mapping,这个拦截器会监测请求参数,并且更改locale。
案例配置
当前如果是*.view的资源包含有siteLanguate参数的都会更改国际化。如下请求路径就会更改语言环境为荷兰语
https://www.sf.net/home.view?siteLanguage=nl
/**/*.view=someController
装载方式
JavaConfig
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleInterceptor());
}
}
xml
源码分析
Spring 国际化初始化的地方
org.springframework.web.servlet.DispatcherServlet#initLocaleResolver
tomcat国际化
我们可以直接调用javax.servlet.ServletRequest#getLocale
获取请求的Locale
国际化-字符
基本概念
字符
各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
也就是说,它是一个信息单位,一个数字是一个字符,一个文字是一个字符,一个标点符号也是一个字符。
字节
字节是一个8bit的存储单元,取值范围是0x00~0xFF。
根据字符编码的不同,一个字符可以是单个字节的,也可以是多个字节的。
字符集
字符的集合就叫字符集。不同集合支持的字符范围自然也不一样,譬如ASCII只支持英文,GB18030支持中文等等
在字符集中,有一个码表的存在,每一个字符在各自的字符集中对应着一个唯一的码。但是同一个字符在不同字符集中的码是不一样的,譬如字符“中”在Unicode和GB18030中就分别对应着不同的码(20013
与54992
)。
字符编码
定义字符集中的字符如何编码为特定的二进制数,以便在计算机中存储。 字符集和字符编码一般一一对应(有例外)
譬如GB18030既可以代表字符集,也可以代表对应的字符编码,它为了兼容ASCII码
,编码方式为code大于255
的采用两位字节(或4字节)来代表一个字符,否则就是兼容模式,一个字节代表一个字符。(简单一点理解,将它认为是现在用的的中文编码就行了)
字符集与字符编码的一个例外就是Unicode字符集,它有多种编码实现(UTF-8,UTF-16,UTF-32等)
字符集和字符编码
字符集(Charset):
是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
字符编码(Character Encoding):
是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。而以计算机为基础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。
常用字符集和字符编码
常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。
计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。
Ascii字符集&编码
ASCII美国信息交换标准代码是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以勉强显示其他西欧语言。它是现今最通用的单字节编码系统(但是有被Unicode追上的迹象),并等同于国际标准ISO/IEC 646。
只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,因此只能用于显示现代美国英语(而且在处理英语当中的外来词如naïve、café、élite等等时,所有重音符号都不得不去掉,即使这样做会违反拼写规则)。而EASCII虽然解决了部份西欧语言的显示问题,但对更多其他语言依然无能为力。
因此现在的苹果电脑已经抛弃ASCII而转用Unicode。
GBXXXX字符集&编码
天朝专家把那些127号之后的奇异符号们(即EASCII)取消掉,规定:
一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。
在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
字符集与字符编码的快速区分
- ASCII码是一个字符集,同时它的实现也只有一种,因此它也可以指代这个字符集对应的字符编码
- GB18030是一个字符集,主要是中国人为了解决中文而发明制定的,由于它的实现也只有一种,所以它也可以指代这个字符集对应的字符编码
- Unicode是一个字符集,为了解决不同字符集码表不一致而推出的,统一了所有字符对应的码,因此在这个规范下,所有字符对应的码都是一致的(统一码),但是统一码只规定了字符与码表的一一对应关系,却没有规定该如何实现,因此这个字符集有多种实现方式(UTF-8,UTF-18,UTF-32),因此这些实现就是对应的字符编码。 也就是说,Unicode统一约定了字符与码表直接一一对应的关系,而UTF-8是Unicode字符集的一种字符编码实现方式,它规定了字符该如何编码成二进制,存储在计算机中。
字符集与字符编码发展简史
欧美的单字节字符编码发展
- 美国人发明了计算机,使用的是英文,所以一开始就设计了一个几乎只支持英文的字符集
ASCII码
(1963 发布),有128个码位,用一个字节即可表示,范围为00000000-01111111
- 后来发现码位不够,于是在这基础上进行拓展,256个字符,取名为
EASCII(Extended ASCII)
,也能一个字节表示,范围为00000000-11111111
- 后来传入欧洲,发现这个标准并不适用于一些欧洲语言,于是在
ASCII
(最原始的ASCII)的基础上拓展,形成了ISO-8859标准(国际标准,1998年发布),跟EASCII类似,兼容ASCII。然后,根据欧洲语言的复杂特性,结合各自的地区语言形成了N个子标准,ISO-8859-1、ISO-8859-2、...
。 兼容性简直令人发指。
亚洲,只能双字节了
计算机传入亚洲后,国际标准已被完全不够用,东亚语言随便一句话就已经超出范围了,也是这时候亚洲各个国家根据自己的地区特色,有发明了自己地图适用的字符集与编码,譬如中国大陆的GB2312,中国台湾的BIG5,日本的Shift JIS等等 这些编码都是用双字节来进行存储,它们对外有一个统称(ANSI-American National Standards Institute),也就是说GB2312或BIG5等都是ANSI在各自地区的不同标准
Unicode,一统天下
- 到了全球互联网时代,不同国家,不同地区需要进行交互,这时候由于各自编码标准都不一样,彼此之间都是乱码,无法良好的沟通交流,于是这时候ISO组织与统一码联盟分别推出了UCS(Universal Multiple-Octet Coded Character Set)与Unicode。后来,两者意识到没有必要用两套字符集,于是进行了一次整合,到了Unicode2.0时代,Nnicode的编码和UCS的编码都基本一致(所以后续为了简便会同意用Unicode指代),这时候所有的字符都可以采用同一个字符集,有着相同的编码,可以愉快的进行交流了。
- 需要注意的是UCS标准有自己的格式,如UCS-2(双字节),UCS-4(四字节)等等 而Unicode也有自己的不同编码实现,如UTF-8,UTF-16,UTF-32等等 其中UTF-16可以认为是UCS-2的拓展,UTF-32可以认为是UCS-4的拓展,而Unicode可以认为是Unicode最终用来制霸互联网的一种编码格式。
在中国,GB系列的发展
- 在计算机传入中国后,1980年,中国国家标准总局发布了第一个汉字编码国家标准GB2312(2312是标准序号),采用双字节编码,里面包括了大部分汉字,拉丁字母,日文假名以及全角字符等。
- 然而,随着程序的发展,逐渐发现GB2312已经不满足需求了,于是1993年又推出了一个GBK编码(汉字国标扩展码),完全兼容GB2312标准。并且包括了BIG5的所有汉字,与1995年发布。 同时GBK也涵盖了Unicode所有CJK汉字,所以也可以和Unicode做一一对应。
- 后来到了2000年,又推出了一个全新的标准 GB 18030,它不仅拓展了新的字符,如支持中国少数名族文字等,而且它采用了单字节,双字节,四字节三种编码方式,所以完全兼容ASCII码与GBK码。 到了2005年,这一标准有进行了拓展,推出了GB18030-2005,剧本涵盖所有汉字,也就是说,现在使用的国标标准码就是GB18030-2005了。
不同字符编码的字符是如何进行转换的
- 如果是相同字符集,由于相同字符集中的码都是一样的,所以只需要针对不同的编码方式转变而已。譬如UTF-16转UTF-8,首先会取到当前需要转换的字符的Unicode码,然后将当前的编码方式由双字节(有4字节的拓展就不赘述了),变为变长的1,2,3等字节
- 如果是不同的字符集,由于不同字符集的码是不一样的,所以需要各自的码表才能进行转换。譬如UTF-16转GBK,首先需要取到当前需要转换的字符的Unicode码,然后根据Unicode和GBK码表一一对应的关系(只有部分共同都有的字符才能在码表中查到),找到它对应的GBK码,然后用GBK的编码方式(双字节)进行编码