java官方文檔html
參考官方文檔java
public class LocaleDemo { public static void main(String[] args) { System.out.println(Locale.getDefault()); } }
獲取本地方言git
經過啓動參數-D命令配置github
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 } }
經過不一樣的方言來決定數字的顯示方式web
建立一個demo_zh_CN.properties
在resources目錄spring
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
因此只要出現的是中文就會亂碼。當前咱們使用的是經過字符串編解碼來轉化的。shell
從上述案例中咱們能夠看到中文會亂碼。json
解決方式有如下三種:windows
jdk
自帶的工具 native2ascii 方法,將打包後的資源文件進行轉移,而不是直接在源碼方面解決擴展 ResourceBundle.Controlapi
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<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
在
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原理具體見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
這裏面不會加載項目中的配置
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
,以便可以接受對上述方法的調用。
org.springframework.context.MessageSourceResolvable
public interface MessageSourceResolvable { String[] getCodes(); Object[] getArguments(); String getDefaultMessage(); }
當前咱們只須要關注這一塊就好了
public interface HierarchicalMessageSource extends MessageSource { void setParentMessageSource(MessageSource parent); MessageSource getParentMessageSource(); }
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"})); } }
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
主要提供了一下幾個功能
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)
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<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);
上述從代碼中能夠看出來模板類主要作了如下幾件事情和提出了一個將來版本或者子類重寫須要優化的地方
提供了模板方法解析消息
org.springframework.context.support.AbstractMessageSource#resolveCodeWithoutArguments
去解析org.springframework.context.support.AbstractMessageSource#resolveArguments
把參數變成參數數組,而後調用子類的org.springframework.context.support.AbstractMessageSource#resolveCode
獲取到MessageFormatProperties
中獲取org.springframework.context.HierarchicalMessageSource
來遞歸調用父類的org.springframework.context.support.AbstractMessageSource#getMessageInternal
查看子類能夠看到其有三個子類
當前類是基於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
中當前類缺點也是很明顯,只能從類路徑讀取,不能指定外部文件
當前類支持相同的包文件格式,但比基於標準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不多使用,相比之下就比較簡單了
@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中獲取值
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(); } }; }
能夠經過LocaleContextHolder類將LocaleContext實例與線程關聯。
org.springframework.context.i18n.LocaleContextHolder
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
請注意,此解析器不支持時區信息
org.springframework.web.servlet.i18n.FixedLocaleResolver
org.springframework.web.servlet.i18n.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>
org.springframework.web.servlet.i18n.SessionLocaleResolver
SessionLocaleResolver容許咱們從可能與用戶請求關聯的會話中檢索Locale和TimeZone。
與CookieLocaleResolver相比,此策略將本地選擇的語言環境設置存儲在Servlet容器的HttpSession中。
所以,這些設置對於每一個會話來講都是臨時的,所以在每一個會話終止時都會丟失。請注意,與外部會話管理機制(如Spring Session項目)沒有直接關係。
該SessionLocaleResolver將僅根據當前的HttpServletRequest評估並修改相應的HttpSession屬性。
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"); }
當咱們收到請求時,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
攔截器給一個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>
@Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LocaleInterceptor()); } }
<mvc:interceptors> <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/> </mvc:interceptors>
Spring 國際化初始化的地方
org.springframework.web.servlet.DispatcherServlet#initLocaleResolver
咱們能夠直接調用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美國信息交換標準代碼是基於拉丁字母的一套電腦編碼系統。它主要用於顯示現代英語,而其擴展版本EASCII則能夠勉強顯示其餘西歐語言。它是現今最通用的單字節編碼系統(可是有被Unicode追上的跡象),並等同於國際標準ISO/IEC 646。
只能顯示26個基本拉丁字母、阿拉伯數目字和英式標點符號,所以只能用於顯示現代美國英語(並且在處理英語當中的外來詞如naïve、café、élite等等時,全部重音符號都不得不去掉,即便這樣作會違反拼寫規則)。而EASCII雖然解決了部份西歐語言的顯示問題,但對更多其餘語言依然無能爲力。
所以如今的蘋果電腦已經拋棄ASCII而轉用Unicode。
天朝專家把那些127號以後的奇異符號們(即EASCII)取消掉,規定:
一個小於127的字符的意義與原來相同,但兩個大於127的字符連在一塊兒時,就表示一個漢字,前面的一個字節(他稱之爲高字節)從0xA1用到 0xF7,後面一個字節(低字節)從0xA1到0xFE,這樣咱們就能夠組合出大約7000多個簡體漢字了。
在這些編碼裏,還把數學符號、羅馬希臘的 字母、日文的假名們都編進去了,連在ASCII裏原本就有的數字、標點、字母都通通從新編了兩個字節長的編碼,這就是常說的"全角"字符,而原來在127號如下的那些就叫"半角"字符了。
ASCII碼
(1963 發佈),有128個碼位,用一個字節便可表示,範圍爲00000000-01111111
EASCII(Extended ASCII)
,也能一個字節表示,範圍爲00000000-11111111
ASCII
(最原始的ASCII)的基礎上拓展,造成了ISO-8859標準(國際標準,1998年發佈),跟EASCII相似,兼容ASCII。而後,根據歐洲語言的複雜特性,結合各自的地區語言造成了N個子標準,ISO-8859-一、ISO-8859-二、...
。 兼容性簡直使人髮指。 計算機傳入亞洲後,國際標準已被徹底不夠用,東亞語言隨便一句話就已經超出範圍了,也是這時候亞洲各個國家根據本身的地區特點,有發明了本身地圖適用的字符集與編碼,譬如中國大陸的GB2312,中國臺灣的BIG5,日本的Shift JIS等等 這些編碼都是用雙字節來進行存儲,它們對外有一個統稱(ANSI-American National Standards Institute),也就是說GB2312或BIG5等都是ANSI在各自地區的不一樣標準