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 } }
通过不同的方言来决定数字的显示方式
创建一个 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
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#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官方地址
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
,以便能够接受对上述方法的调用。
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
获取到MessageFormat Properties
中获取 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
官方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
请注意,此解析器不支持时区信息
org.springframework.web.servlet.i18n.FixedLocaleResolver
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文档地址
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
参考文档地址
参考地址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美国信息交换标准代码是基于 拉丁字母 的一套 电脑 编码 系统。它主要用于显示 现代英语 ,而其扩展版本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-1、ISO-8859-2、...
。 兼容性简直令人发指。 计算机传入亚洲后,国际标准已被完全不够用,东亚语言随便一句话就已经超出范围了,也是这时候亚洲各个国家根据自己的地区特色,有发明了自己地图适用的字符集与编码,譬如中国大陆的GB2312,中国台湾的BIG5,日本的Shift JIS等等 这些编码都是用双字节来进行存储,它们对外有一个统称(ANSI-American National Standards Institute),也就是说GB2312或BIG5等都是ANSI在各自地区的不同标准