Java原生國際化
文檔位址
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
是以隻要出現的是中文就會亂碼。目前我們使用的是通過字元串編解碼來轉化的。
國際化亂碼問題
從上述案例中我們可以看到中文會亂碼。
解決方式有以下三種:
- 可以采用
自帶的工具 native2ascii 方法,将打包後的資源檔案進行轉移,而不是直接在源碼方面解決jdk
- 擴充 ResourceBundle.Control
- 缺點:可移植性不強,不得不顯示地傳遞
- 實作 ResourceBundleControlProvider
jdk自帶的 native2ascii
native2ascii
工具文檔位址
java支援的編碼
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
從
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<InputStream>() {
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<InputStream>() {
@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
ResourceBundleControlProvider
在
static {
List<ResourceBundleControlProvider> list = null;
ServiceLoader<ResourceBundleControlProvider> 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官方位址
spi原理具體見
java.util.ServiceLoader.LazyIterator#hasNextService
編寫代碼
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-消息轉化的頂層接口
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<String, Map<Locale, MessageFormat>> messageFormatsPerMessage =
new HashMap<String, Map<Locale, MessageFormat>>();
//使用緩存的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<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
if (messageFormatsPerLocale != null) {
messageFormat = messageFormatsPerLocale.get(locale);
}
else {
messageFormatsPerLocale = new HashMap<Locale, MessageFormat>();
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
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<Object> resolvedArgs = new ArrayList<Object>(args.length);
for (Object arg : args) {
if (arg instanceof MessageSourceResolvable) {
resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale));
}
else {
resolvedArgs.add(arg);
}
}
return resolvedArgs.toArray(new Object[resolvedArgs.size()]);
}
protected String resolveCodeWithoutArguments(String code, Locale locale) {
//直接調用子類的解析方式
MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {
synchronized (messageFormat) {
return messageFormat.format(new Object[0]);
}
}
return null;
}
protected abstract MessageFormat resolveCode(String code, Locale locale);
上述從代碼中可以看出來模闆類主要做了以下幾件事情和提出了一個未來版本或者子類重寫需要優化的地方
- 提供了模闆方法解析消息
- 對于沒有args的并且參數為空的直接交給子類重寫的
去解析org.springframework.context.support.AbstractMessageSource#resolveCodeWithoutArguments
- 其他的則先
把參數變成參數數組,然後調用子類的org.springframework.context.support.AbstractMessageSource#resolveArguments
擷取到MessageFormatorg.springframework.context.support.AbstractMessageSource#resolveCode
- 如果都沒有傳回對應的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<String> 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<String> 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<Locale, ResourceBundle> 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<Locale, ResourceBundle>();
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<String> 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<String> 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<String> 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<String> calculateAllFilenames(String basename, Locale locale) {
Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
if (localeMap != null) {
List<String> filenames = localeMap.get(locale);
if (filenames != null) {
return filenames;
}
}
List<String> filenames = new ArrayList<String>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
if (isFallbackToSystemLocale() && !locale.equals(Locale.getDefault())) {
List<String> 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<Locale, List<String>>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
return filenames;
}
//計算給定包基本名稱和語言環境的檔案名
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
List<String> result = new ArrayList<String>(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<String> 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
官方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
CookieLocaleResolver文檔位址
此區域設定解析器檢查用戶端上可能存在的Cookie,以檢視是否指定了區域設定或時區。如果是,則使用指定的詳細資訊。使用此區域設定解析器的屬性,可以指定cookie的名稱以及存活時間。下面是定義CookieLocaleResolver的一個示例。
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
<property name="cookieMaxAge" value="100000"/>
</bean>
SessionLocaleResolver
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
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
裝載方式
JavaConfig
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleInterceptor());
}
}
xml
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
</mvc:interceptors>
源碼分析
Spring 國際化初始化的地方
org.springframework.web.servlet.DispatcherServlet#initLocaleResolver
tomcat國際化
我們可以直接調用
javax.servlet.ServletRequest#getLocale
擷取請求的Locale
國際化-字元
參考文檔位址
參考位址2
基本概念
字元
各種文字和符号的總稱,包括各國家文字、标點符号、圖形符号、數字等。
也就是說,它是一個資訊機關,一個數字是一個字元,一個文字是一個字元,一個标點符号也是一個字元。
位元組
位元組是一個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字元集的一種字元編碼實作方式,它規定了字元該如何編碼成二進制,存儲在計算機中。
字元集與字元編碼發展簡史
歐美的單位元組字元編碼發展
- 美國人發明了計算機,使用的是英文,是以一開始就設計了一個幾乎隻支援英文的字元集
(1963 釋出),有128個碼位,用一個位元組即可表示,範圍為ASCII碼
00000000-01111111
- 後來發現碼位不夠,于是在這基礎上進行拓展,256個字元,取名為
,也能一個位元組表示,範圍為EASCII(Extended ASCII)
00000000-11111111
- 後來傳入歐洲,發現這個标準并不适用于一些歐洲語言,于是在
(最原始的ASCII)的基礎上拓展,形成了ISO-8859标準(國際标準,1998年釋出),跟EASCII類似,相容ASCII。然後,根據歐洲語言的複雜特性,結合各自的地區語言形成了N個子标準,ASCII
。 相容性簡直令人發指。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的編碼方式(雙位元組)進行編碼
代碼位址