文章目录

  • 前言
  • 一、Java中的国际化原理分析
  • 二、一些痛点以及解决方案
    • 1.编码问题
    • 2.占位符问题
  • 三、行业内优秀的解决方案
  • 总结

前言

国际化这个词听起来很高大上,其实所谓的国际化就是针对不同的语言、地区采用不同的提示,比如遇见中国人就是“你好”,如果是美国人就是“Hello”,因为这两个国家的母语不一样,然后即使是同一个国家的不同地区,语言也有可能不一样,比如在中国大陆地区,使用的是简体,而在台湾地区,使用的就是繁体。在Java中处理国际化的主要类是java.util.ResourceBundle 这个类,下面我们针对这个类的用法以及一些问题进行分析


提示:以下是本篇文章正文内容,下面案例可供参考

一、Java中的国际化原理分析

Java中的国际化使用的是java.util.ResourceBundle,比如在Tomcat的有如下这个类
org.apache.naming.StringManager

/*** Creates a new StringManager for a given package. This is a* private method and all access to it is arbitrated by the* static getManager method call so that only one StringManager* per package will be created.** @param packageName Name of package to create StringManager for.*/
private StringManager(String packageName) {String bundleName = packageName + ".LocalStrings";ResourceBundle tempBundle = null;try {tempBundle = ResourceBundle.getBundle(bundleName, Locale.getDefault());} catch( MissingResourceException ex ) {// Try from the current loader (that's the case for trusted apps)// Should only be required if using a TC5 style classloader structure// where common != shared != serverClassLoader cl = Thread.currentThread().getContextClassLoader();if( cl != null ) {try {tempBundle = ResourceBundle.getBundle(bundleName, Locale.getDefault(), cl);} catch(MissingResourceException ex2) {// Ignore}}}// Get the actual locale, which may be different from the requested oneif (tempBundle != null) {locale = tempBundle.getLocale();} else {locale = null;}bundle = tempBundle;
}

以上这个类的构造方法通过传入指定的包名,然后加载其中的国际化配置文件,如下图所示:

可以看到这里有三个文件,类型都是properties,而且前缀部分都是LocalStrings,还有中间带有fr的,意思为法国,而ja,代表日本。国际化的目标就是根据不同的环境(Locale)读取不同的配置文件。如果当前环境是法国,就读这个LocalStrings_fr.propertis文件,如果是日本,就读取LocalStrings_ja.propertis文件。

比如我们定义了一个rb.properties文件,文件内容如下

first=Congratulations and best wishes for a prosperous New Year!
second=Congratulations and best wishes for a prosperous New Year!
third=welcome!{0}

然后通过ResourceBundle类去加载并读取其中的值

@Test
public void testSimple() {// 资源名称String baseName = "rb";// 读取文件ResourceBundle resourceBundle = ResourceBundle.getBundle(baseName);// 获取所有的key值Enumeration<String> bundleKeys = resourceBundle.getKeys();while (bundleKeys.hasMoreElements()) {// 遍历String nextElement = bundleKeys.nextElement();// 根据key值获取目标值System.out.println("key = " + nextElement + " ,value = " + resourceBundle.getString(nextElement));}
}


控制台打印如下

key = third ,value = welcome!{0}
key = first ,value = Congratulations and best wishes for a prosperous New Year!
key = second ,value = Congratulations and best wishes for a prosperous New Year!

然后我们再添加两个文件,分别为
rb_zh.properties

first=\u606d\u559c\u53d1\u8d22
second=恭喜发财
third=欢迎你!{0}!


rb_zh_TW.properties

first=\u606d\u559c\u767c\u8ca1
second=恭喜發財
third=欢迎你!{0}!

然后再跑上面的测试,测试结果如下

key = third ,value = æ¬¢è¿Žä½ ï¼{0}!
key = first ,value = 恭喜发财
key = second ,value = 恭喜发财

可以看到此时读取的值与上面的不同,而且有乱码,只有第一个值是正常的。因为key为first的值其实为ASII码值。ResourceBundle默认使用的是ISO-8859-1编码,读取英文外的其他语言可能会导致乱码,最简单的解决方法当然就是通过把字符都写成ASII码解决问题,在jdk中有相关工具用于转码,一些网页里面的工具类会更方便。比如:https://www.sojson.com/ascii.html,比如恭喜发财四个字的ASII码就是\u606d\u559c\u53d1\u8d22,这种方法虽然简单,但是给维护工作带来很大的麻烦,首先这个ASII码很难辨认,其次想修改其中一个字,比如又字,需要重新生成一遍。然后在ResourceBundle目前提供的方法中并没有关于编码CharSet的参数,这也是第一个痛点所在,后面会提到。

我们依旧回到关于国际化的问题上。分析一下国际化的原理。我们编写一个新的测试

@Test
public void testResourceBundle() {// 不能带后缀String baseName = "rb";Locale locale = Locale.getDefault();System.out.println("locale = " + locale);System.out.println("language = " + locale.getLanguage());System.out.println("country = " + locale.getCountry());// 根据Locale加载文件ResourceBundle resourceBundle = ResourceBundle.getBundle(baseName, locale);String baseBundleName = resourceBundle.getBaseBundleName();String displayName = resourceBundle.getLocale().getDisplayName();System.out.println("baseBundleName = " + baseBundleName);System.out.println("locale displayName = " + displayName);Enumeration<String> bundleKeys = resourceBundle.getKeys();while (bundleKeys.hasMoreElements()) {String nextElement = bundleKeys.nextElement();System.out.println("key = " + nextElement + " ,value = " + resourceBundle.getString(nextElement));}
}

可以看到现在的运行结果其实与前面是一样的。

修改一下LocaleTRADITIONAL_CHINESE.

/** Useful constant for language.*/
static public final Locale CHINESE = createConstant("zh", "");/** Useful constant for language.*/
static public final Locale SIMPLIFIED_CHINESE = createConstant("zh", "CN");/** Useful constant for language.*/
static public final Locale TRADITIONAL_CHINESE = createConstant("zh", "TW");


此时读取的值为rb_zh_TW.properties中的值。在上面测试中我们读取了Locale中的两个值,一个是language(语言),一个是country(地区),在默认情况下,其实Locale的取值为SIMPLIFIED_CHINESE,此时首先去查找rb_zh_CN.properties这个文件的,但是无法查找到,然后就会降级去读rb_zh.properties这个文件。而设置为TRADITIONAL_CHINESE时,就直接读取了rb_zh_TW.properties这个文件。

根据以上结果,我们得出(错误)结论ResourceBundle读取文件的先后顺序是

  • 首先根据语言+地区读取配置文件,比如rb_zh_TW.properties,其中zh为语言,TW为地区
  • 如果以上步骤读取不到,然后再根据语言读取配置文件,比如rb_zh.properties
  • 如果以上步骤读取不到,就会读取默认的配置文件(不包含语言和地区),比如rb.properties。
  • 还读取不到,则会抛出异常,比如去加载baseName为rb1的配置信息

不妨想一个问题,如果根据Locale对应的文件当中不包含指定的key会怎么样呢?

可以看到此时的异常为:java.util.MissingResourceException: Can't find resource for bundle java.util.PropertyResourceBundle, key four

我们针对rb.properties做出修改

此时可以读取到刚刚配置的值

这是不是跟我们刚才得出的结论有点出入吗?因为我们现在只是在rb.properties文件中添加了相关配置,而不是rb_zh_TW.properties这个文件中。上面我们的分析初次听起来是没有任何问题的,这也是很多人的见解,但是如果一知半解,止步于此,我们觉得自己懂了,也通过一些方法验证了,那么就大错特错了。我们的认识可以说差之毫厘谬以千里了。
首先上一张图

这张图,是我们根据语言+地区(TRADITIONAL_CHINESE)去获取的ResourceBundle类信息,其中localekeySet属性不解释了,最主要的是那个parent属性,从上图不难看出,这个parent类型也是ResourceBundle,而且这个parent还有parent,类型也是ResourceBundle。所以其实在ResourceBundle根据baseNamelocale去创建ResourceBundle对象的时候,会把locale等级低的都加载并根据等级构造父子关系,比如上面的就是zh_TW->zh->default的顺序构造了父子层级关系,在读取key值的时候,如果子不存在,就从父里面读,直到读到为止,如果没有读到值,就会抛出异常。

主要参考源码java.util.ResourceBundle#getBundleImpl最主要部分(其余部分主要为缓存或者异常处理等)

ResourceBundle baseBundle = null;
// 循环
for (Locale targetLocale = locale;targetLocale != null;targetLocale = control.getFallbackLocale(baseName, targetLocale)) {// 根据目标Locale获取可用的LocaleList<Locale> candidateLocales = control.getCandidateLocales(baseName, targetLocale);if (!isKnownControl && !checkList(candidateLocales)) {throw new IllegalArgumentException("Invalid Control: getCandidateLocales");}// 查找目标ResourceBundle 注意这里将candidateLocales传入了bundle = findBundle(cacheKey, candidateLocales, formats, 0, control, baseBundle);// If the loaded bundle is the base bundle and exactly for the// requested locale or the only candidate locale, then take the// bundle as the resulting one. If the loaded bundle is the base// bundle, it's put on hold until we finish processing all// fallback locales.if (isValidBundle(bundle)) {boolean isBaseBundle = Locale.ROOT.equals(bundle.locale);if (!isBaseBundle || bundle.locale.equals(locale)|| (candidateLocales.size() == 1&& bundle.locale.equals(candidateLocales.get(0)))) {break;}// If the base bundle has been loaded, keep the reference in// baseBundle so that we can avoid any redundant loading in case// the control specify not to cache bundles.if (isBaseBundle && baseBundle == null) {baseBundle = bundle;}}
}

首先这里有一步是根据目标Locale获取可用的Locale,如下图所示

然后通过findBundle查找目标ResourceBundle,注意此处传入了candidateLocales列表。该方法就是构建父子关系的关键方法。通过递归该该方法获取parent,而真正读取文件则是在java.util.ResourceBundle#loadBundle方法当中。

对应列表为index+1,这里的层级关系在candidateLocales列表中决定了。在列表前面的优先级高(只是读取key值时等级高、但加载配置时其实是在后面的,因为排在后面的需要先加载作为parent),在列表后面的优先级低(但是在父子层级关系中等级高、文件优先读取,加载文件构造的ResourceBundle对象作为队列前面元素的parent)。对于最后一个元素,如果baseBundle不为空而且targetLocale为空的话,则返回baseBundle作为父子层级关系的最后一层。

private static ResourceBundle findBundle(CacheKey cacheKey,List<Locale> candidateLocales,List<String> formats,int index,Control control,ResourceBundle baseBundle) {Locale targetLocale = candidateLocales.get(index);ResourceBundle parent = null;if (index != candidateLocales.size() - 1) {// 递归查询parent,parent为获取高一级的parent = findBundle(cacheKey, candidateLocales, formats, index + 1,control, baseBundle);} else if (baseBundle != null && Locale.ROOT.equals(targetLocale)) {return baseBundle;}// Before we do the real loading work, see whether we need to// do some housekeeping: If references to class loaders or// resource bundles have been nulled out, remove all related// information from the cache.Object ref;while ((ref = referenceQueue.poll()) != null) {cacheList.remove(((CacheKeyReference)ref).getCacheKey());}// flag indicating the resource bundle has expired in the cacheboolean expiredBundle = false;// First, look up the cache to see if it's in the cache, without// attempting to load bundle.cacheKey.setLocale(targetLocale);ResourceBundle bundle = findBundleInCache(cacheKey, control);if (isValidBundle(bundle)) {expiredBundle = bundle.expired;if (!expiredBundle) {// If its parent is the one asked for by the candidate// locales (the runtime lookup path), we can take the cached// one. (If it's not identical, then we'd have to check the// parent's parents to be consistent with what's been// requested.)if (bundle.parent == parent) {return bundle;}// Otherwise, remove the cached one since we can't keep// the same bundles having different parents.BundleReference bundleRef = cacheList.get(cacheKey);if (bundleRef != null && bundleRef.get() == bundle) {cacheList.remove(cacheKey, bundleRef);}}}if (bundle != NONEXISTENT_BUNDLE) {CacheKey constKey = (CacheKey) cacheKey.clone();try {// 加载当前配置文件bundle = loadBundle(cacheKey, formats, control, expiredBundle);if (bundle != null) {if (bundle.parent == null) {// 设置父子关系bundle.setParent(parent);}// 设置对象的locale信息 bundle.locale = targetLocale;bundle = putBundleInCache(cacheKey, bundle, control);// 返回作为队列中前一个元素的parent 如果队列的第一个元素 则解析完成 return bundle;}// Put NONEXISTENT_BUNDLE in the cache as a mark that there's no bundle// instance for the locale.putBundleInCache(cacheKey, NONEXISTENT_BUNDLE, control);} finally {if (constKey.getCause() instanceof InterruptedException) {Thread.currentThread().interrupt();}}}return parent;
}

根据不同的format加载目标文件,此处为java.properties方式

最终读取文件的方式其实也很简单(当前的资源名称为rb_zh_TW.properties

读取文件获取到输入流,根据输入流构造PropertyResourceBundle类型对象,并关闭流

创建PropertyResourceBundle对象之后,赋值一些基础属性name和locale.

当然最后还需要设置属性parent构造父子关系。然后再返回自己作为队列中前一个元素对应的ResourceBundle对象的parent。

另外在构造父子关系的时候,如果处于中间一级的没有配置文件,怎么办呢?这里也很巧妙,在查找配置文件的时候,如果当前的bundle不存在,然后就会返回parent

而如果最后一层locale如果不存在对应的配置文件,那么就会返回null。

如果下一个层级有对应的配置文件,那么设置的parent就是空了。

接下来就是查找值了,对应的逻辑如下,这个还是比较简单的

/*** Gets an object for the given key from this resource bundle or one of its parents.* This method first tries to obtain the object from this resource bundle using* {@link #handleGetObject(java.lang.String) handleGetObject}.* If not successful, and the parent resource bundle is not null,* it calls the parent's <code>getObject</code> method.* If still not successful, it throws a MissingResourceException.** @param key the key for the desired object* @exception NullPointerException if <code>key</code> is <code>null</code>* @exception MissingResourceException if no object for the given key can be found* @return the object for the given key*/
public final Object getObject(String key) {Object obj = handleGetObject(key);if (obj == null) {if (parent != null) {obj = parent.getObject(key);}if (obj == null) {throw new MissingResourceException("Can't find resource for bundle "+this.getClass().getName()+", key "+key,this.getClass().getName(),key);}}return obj;
}


最后总结一下:在getBundle的过程中,首先系统根据当前请求目标targetLocale获取所有可支持的Locale,也就是candidateLocales列表,在这个表中定义了相应的层级关系,当然这个层级关系也决定了可能需要被加载的文件,然后根据层级关系去加载对应文件构造ResourceBundle对象并设置父子关系,在读取配置文件中key值的时候首先读取当前ResourceBundle对象中名称为lookup的Map中的值,其实就是根据key值查找value值(数据结构为map),如果不存在,则依次遍历父层级获取值,如果到最后查找不到,则抛出异常。

二、一些痛点以及解决方案

1.编码问题

在上面分析读取原理的时候,如果加载的文件中包含中文会出现乱码的问题,需要如何去解决呢?首先我们可以看到上面加载properies文件之后创建的其实为java.util.PropertyResourceBundle类型。查看这个类的构造方法

/*** Creates a property resource bundle from an {@link java.io.InputStream* InputStream}.  The property file read with this constructor* must be encoded in ISO-8859-1.** @param stream an InputStream that represents a property file*        to read from.* @throws IOException if an I/O error occurs* @throws NullPointerException if <code>stream</code> is null* @throws IllegalArgumentException if {@code stream} contains a*     malformed Unicode escape sequence.*/
@SuppressWarnings({"unchecked", "rawtypes"})
public PropertyResourceBundle (InputStream stream) throws IOException {Properties properties = new Properties();properties.load(stream);lookup = new HashMap(properties);
}/*** Creates a property resource bundle from a {@link java.io.Reader* Reader}.  Unlike the constructor* {@link #PropertyResourceBundle(java.io.InputStream) PropertyResourceBundle(InputStream)},* there is no limitation as to the encoding of the input property file.** @param reader a Reader that represents a property file to*        read from.* @throws IOException if an I/O error occurs* @throws NullPointerException if <code>reader</code> is null* @throws IllegalArgumentException if a malformed Unicode escape sequence appears*     from {@code reader}.* @since 1.6*/
@SuppressWarnings({"unchecked", "rawtypes"})
public PropertyResourceBundle (Reader reader) throws IOException {Properties properties = new Properties();properties.load(reader);lookup = new HashMap(properties);
}

对于第一个构造方法(输入参数类型为InputStream的),看注解强制要求编码必须为ISO 8859-1````,因为在构造方法中通过java.util.Properties#load(java.io.InputStream)来加载流数据,如果不是ISO 8859-1````的编码就会抛出IllegalArgumentException异常。但是第二个构造方法给我们机会了。

@Test
public void testPropertiesResourceBundle() throws IOException {// 不能带后缀String baseName = "rb";String ext = ".properties";
//        // 设置Locale根据Locale查找Locale locale = Locale.TRADITIONAL_CHINESE;// 按照语言和地区查找InputStream asStream = ClassLoader.getSystemClassLoader().getResourceAsStream(baseName + "_" + locale + ext);PropertyResourceBundle resourceBundle = new PropertyResourceBundle(new InputStreamReader(asStream, StandardCharsets.UTF_8));Enumeration<String> bundleKeys = resourceBundle.getKeys();while (bundleKeys.hasMoreElements()) {String nextElement = bundleKeys.nextElement();System.out.println("key = " + nextElement + " ,value = " + resourceBundle.getString(nextElement));}
}

通过上面的方法我们通过utf-8读取了文件。测试结果如下

看起来是解决了编码的问题,但是如果看懂了第一部分的原理就会发现,其实这跟本没有达到目标,因为此时的resourceBundle对象丢掉了最重要的父子层级关系。

必须强调一下,这里以UTF-8编码解决乱码的前提是配置文件首先必须是UTF-8编码的,因为在不少的编程环境当中默认的文件编码是GBK,这个时候通过UTF-8去加载文件是不可能解决乱码的,必须通过对应的编码。如果你的环境是GBK,那就用GBK.

2.占位符问题

在上面的案例中,不知道你是否有发现值是以下这种的包含有占位符信息的

key = third ,value = 欢迎你!{0}!

这种格式主要是先用占位符占个位置,在代码中动态获取值并填充。通过ResourceBundle是没办法完成这个工作的,还必须要另外一个类java.text.MessageFormat才可以。

PropertyResourceBundle resourceBundle = new PropertyResourceBundle(new InputStreamReader(asStream, StandardCharsets.UTF_8));
String third = resourceBundle.getString("third");
MessageFormat messageFormat = new MessageFormat(third);
String jack = messageFormat.format(new Object[]{"jack"});
System.out.println(jack);


三、行业内优秀的解决方案

关于国际化可能大家都知道,但是关于ResourceBundle这个类以及对应的痛点可能就知之甚少了,为什么呢?其实这是Spring的强大之处了,Spring非常完美的解决了以上的问题。Spring主要是通过org.springframework.context.MessageSource这个接口来解决国际化问题的,按照官方的说法(参数化和国际化)

Strategy interface for resolving messages, with support for the parameterization and internationalization of such messages.

主要方法如下

其中最主要的两个类就是ResourceBundleMessageSourceReloadableResourceBundleMessageSource,我们今天主从ResourceBundleMessageSource来讲Spring是如何解决原生ResourceBundle国际化的问题的。首先看一下ResourceBundleMessageSource是如何使用的。

ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 加载名称为rs的国际化资源文件
messageSource.setBasenames("rs");
// 设置默认的编码为UTF-8
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
// 获取指定的key值 并传入占位符中的值 类型为数组 并指定Locale值
String message = messageSource.getMessage("message", new Object[]{"mark"}, Locale.SIMPLIFIED_CHINESE);
System.out.println(message);


Spring提供非常方便的方式设置编码、设置Locale值、以及传入参数填充占位符。那么Spring是如何做到的呢?
首先看一下这个类的层级关系

首先这个类继承了org.springframework.context.support.AbstractMessageSource,调用getMessage就是调用这个抽象类的方法

@Override
public final String getMessage(String code, @Nullable 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);
}

然后调用方法getMessageInternal

我们首先关注包含占位符参数的

// Resolve arguments eagerly, for the case where the message
// is defined in a parent MessageSource but resolvable arguments
// are defined in the child MessageSource.
argsToUse = resolveArguments(args, locale);MessageFormat messageFormat = resolveCode(code, locale);
if (messageFormat != null) {synchronized (messageFormat) {return messageFormat.format(argsToUse);}
}

重点是resolveCode方法,这个方法是在ResourceBundleMessageSource类中实现的,其实上面没有参数的时候调用的方法resolveCodeWithoutArguments也是在ResourceBundleMessageSource中实现的。参考下图,这两个方法中最关键的步骤就是读取配置文件,这里我们见到了java.util.ResourceBundle这个类了,可以看出Spring最终也是围绕ResourceBundle这个类的,这里就是通过getResourceBundle来构建对象。

getResourceBundle方法中绝大部分也是在于缓存打交道,我们可以忽略,最关键是doGetBundle方法。

protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {ClassLoader classLoader = getBundleClassLoader();Assert.state(classLoader != null, "No bundle ClassLoader set");MessageSourceControl control = this.control;if (control != null) {try {return ResourceBundle.getBundle(basename, locale, classLoader, control);}catch (UnsupportedOperationException ex) {// Probably in a Jigsaw environment on JDK 9+this.control = null;String encoding = getDefaultEncoding();if (encoding != null && logger.isInfoEnabled()) {logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" +encoding + "' but ResourceBundle.Control not supported in current system environment: " +ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " +"platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " +"for participating in the platform default and therefore avoiding this log message.");}}}// Fallback: plain getBundle lookup without Control handlereturn ResourceBundle.getBundle(basename, locale, classLoader);
}

上面的这段代码是不是非常的熟悉了。除了Spring使用了自己扩展的MessageSourceControl,都是JDK的那一套。所以我们仔细看看MessageSourceControl这个类,也是通过继承JDK中的类只是简单覆盖了newBundle这个方法。

加载文件获取流的过程与JDK基本一致的,只是在获取流之后,如果设置了默认的编码,则根据编码创建Reader对象,这跟我们上面创建PropertyResourceBundle是一样的。

我们再看一下org.springframework.context.support.ResourceBundleMessageSource#loadBundle(java.io.Reader)方法,与我们上面是一样的。

protected ResourceBundle loadBundle(Reader reader) throws IOException {return new PropertyResourceBundle(reader);
}

但是Spring通过这种扩展ResourceBundle.Control方式能够保证建立好各种层级关系(没有打破原来加载可用的Locale信息以及根据可用的Locale遍历加载文件设置父子层级关系,因为还是JDK的那一套代码),不得不佩服Spring的强大呀,对JDK源码的熟悉非常重要

在获取了ResourceBundle对象之后,接下来就是通过MessageFormat去填充占位符了,除去绝大部分的缓存逻辑,主要就以下两个重要的地方,而createMessageFormat方法就是简单的创建MessageFormat对象,然后调用format方法。

/*** Create a MessageFormat for the given message and Locale.* @param msg the message to create a MessageFormat for* @param locale the Locale to create a MessageFormat for* @return the MessageFormat instance*/
protected MessageFormat createMessageFormat(String msg, Locale locale) {return new MessageFormat(msg, locale);
}


从以上源码不难看出,Spring针对ResourceBundle的痛点解决的非常完美,而且出其的简单。


总结

针对国际化,Java提供了ResourceBundle这个类,但是在编码问题和格式化占位符的问题上就差临门一脚了,Spring针对这些问题首先使用了另一个接口MessageSource,在其实现类ResourceBundleMessageSource中通过简单的扩展解决了以上的问题(与Spring中创建Resource接口解析资源加载问题很相似,Spring不离标准,却在不知不觉中构建自己的标准),但是说到底,其实还是Java原生的那一套,看起来简单,但是要做到如此简单而又不产生其他问题就需要对JDK源码非常深刻的理解了。另外Spring还解决了另一个问题,就是可以从多个国际化资源中读取值,只需要在messageSource.setBasenames方法中传入多个值就好了,这样只需要一个messageSource对象,否则如果用ResourceBundle,多种国际化资源文件用户就需要面对多个ResourceBundle对象了。比如

messageSource.addBasenames("rb", "rb1", "rb2")

以下是类ResourceBundle上的注释,反过头来仔细阅读将会有很大的收获。嗯,英文不妨多读读。实在有困难,就用翻译工具吧,我保证你会对ResourceBundle有更深入的认识,比如SPI机制、缓存失效等。

Resource bundles contain locale-specific objects. When your program needs a locale-specific
resource, a String for example, your program can load it from the resource bundle that is
appropriate for the current user's locale. In this way, you can write program code that is
largely independent of the user's locale isolating most, if not all, of the locale-specific
information in resource bundles.This allows you to write programs that can:
1. be easily localized, or translated, into different languages
2. handle multiple locales at once
3. be easily modified later to support even more localesResource bundles belong to families whose members share a common base name, but whose names
also have additional components that identify their locales. For example, the base name of
a family of resource bundles might be "MyResources". The family should have a default resource
bundle which simply has the same name as its family - "MyResources" - and will be used as the
bundle of last resort if a specific locale is not supported. The family can then provide as
many locale-specific members as needed, for example a German one named "MyResources_de".Each resource bundle in a family contains the same items, but the items have been translated for
the locale represented by that resource bundle. For example, both "MyResources" and
"MyResources_de" may have a String that's used on a button for canceling operations.
In "MyResources" the String may contain "Cancel" and in "MyResources_de" it may contain "Abbrechen".If there are different resources for different countries, you can make specializations:
for example, "MyResources_de_CH" contains objects for the German language (de) in Switzerland (CH).
If you want to only modify some of the resources in the specialization, you can do so.When your program needs a locale-specific object, it loads the ResourceBundle class using the
getBundle method:ResourceBundle myResources = ResourceBundle.getBundle("MyResources", currentLocale);Resource bundles contain key/value pairs. The keys uniquely identify a locale-specific object
in the bundle. Here's an example of a ListResourceBundle that contains two key/value pairs:
public class MyResources extends ListResourceBundle {protected Object[][] getContents() {return new Object[][] {// LOCALIZE THE SECOND STRING OF EACH ARRAY (e.g., "OK"){"OkKey", "OK"},{"CancelKey", "Cancel"},// END OF MATERIAL TO LOCALIZE};}
}Keys are always Strings. In this example, the keys are "OkKey" and "CancelKey". In the above
example, the values are also Strings--"OK" and "Cancel"--but they don't have to be. The values
can be any type of object.You retrieve an object from resource bundle using the appropriate getter method. Because "OkKey"
and "CancelKey" are both strings, you would use getString to retrieve them:
button1 = new Button(myResources.getString("OkKey"));
button2 = new Button(myResources.getString("CancelKey"));The getter methods all require the key as an argument and return the object if found. If the
object is not found, the getter method throws a MissingResourceException.Besides getString, ResourceBundle also provides a method for getting string arrays, getStringArray,
as well as a generic getObject method for any other type of object. When using getObject, you'll
have to cast the result to the appropriate type. For example:
int[] myIntegers = (int[]) myResources.getObject("intList");The Java Platform provides two subclasses of ResourceBundle, ListResourceBundle and
PropertyResourceBundle, that provide a fairly simple way to create resources. As you saw briefly
in a previous example, ListResourceBundle manages its resource as a list of key/value pairs.
PropertyResourceBundle uses a properties file to manage its resources.If ListResourceBundle or PropertyResourceBundle do not suit your needs, you can write your own
ResourceBundle subclass. Your subclasses must override two methods: handleGetObject and getKeys().The implementation of a ResourceBundle subclass must be thread-safe if it's simultaneously used by
multiple threads. The default implementations of the non-abstract methods in this class, and the
methods in the direct known concrete subclasses ListResourceBundle and PropertyResourceBundle are
thread-safe.ResourceBundle.ControlThe ResourceBundle.Control class provides information necessary to perform the bundle loading process
by the getBundle factory methods that take a ResourceBundle.Control instance. You can implement your
own subclass in order to enable non-standard resource bundle formats, change the search strategy,
or define caching parameters. Refer to the descriptions of the class and the getBundle factory method
for details.For the getBundle factory methods that take no ResourceBundle.Control instance, their default behavior
of resource bundle loading can be modified with installed ResourceBundleControlProvider implementations.
Any installed providers are detected at the ResourceBundle class loading time. If any of the providers
provides a ResourceBundle.Control for the given base name, that ResourceBundle.Control will be used
instead of the default ResourceBundle.Control. If there is more than one service provider installed for
supporting the same base name, the first one returned from ServiceLoader will be used.Cache ManagementResource bundle instances created by the getBundle factory methods are cached by default, and the factory
methods return the same resource bundle instance multiple times if it has been cached. getBundle clients
may clear the cache, manage the lifetime of cached resource bundle instances using time-to-live values,
or specify not to cache resource bundle instances. Refer to the descriptions of the getBundle factory method,
clearCache, ResourceBundle.Control.getTimeToLive, and ResourceBundle.Control.needsReload for details.
Example
The following is a very simple example of a ResourceBundle subclass, MyResources, that manages two resources
(for a larger number of resources you would probably use a Map). Notice that you don't need to supply a value
if a "parent-level" ResourceBundle handles the same key with the same value (as for the okKey below).// default (English language, United States)public class MyResources extends ResourceBundle {public Object handleGetObject(String key) {if (key.equals("okKey")) return "Ok";if (key.equals("cancelKey")) return "Cancel";return null;}public Enumeration<String> getKeys() {return Collections.enumeration(keySet());}// Overrides handleKeySet() so that the getKeys() implementation// can rely on the keySet() value.protected Set<String> handleKeySet() {return new HashSet<String>(Arrays.asList("okKey", "cancelKey"));}}// German languagepublic class MyResources_de extends MyResources {public Object handleGetObject(String key) {// don't need okKey, since parent level handles it.if (key.equals("cancelKey")) return "Abbrechen";return null;}protected Set<String> handleKeySet() {return new HashSet<String>(Arrays.asList("cancelKey"));}}You do not have to restrict yourself to using a single family of ResourceBundles. For example, you could have
a set of bundles for exception messages, ExceptionResources (ExceptionResources_fr, ExceptionResources_de, ...),
and one for widgets, WidgetResource (WidgetResources_fr, WidgetResources_de, ...); breaking up the resources however
you like.

关于ResourceBundle国际化的一些思考相关推荐

  1. java bundle_java.util.ResourceBundle国际化用法详解

    初识国际化和ResourceBundle 这个类主要用来解决国际化和本地化问题.国际化和本地化可不是两个概念,两者都是一起出现的.可以说,国际化的目的就是为了实现本地化,详细的介绍可以看本文的最后.比 ...

  2. ResourceBundle国际化

    Code: import java.util.ResourceBundle; public class ResourceBundleDemo { public static void main(Str ...

  3. java.util.ResourceBundle使用详解

    为什么80%的码农都做不了架构师?>>>      2009-07-29 00:47:17     一.认识国际化资源文件 这个类提供软件国际化的捷径.通过此类,可以使您所编写的程序 ...

  4. 【转】java.util.ResourceBundle使用详解

    原文链接:http://lavasoft.blog.51cto.com/62575/184605/ 人家写的太好了,条理清晰,表达准确. 一.认识国际化资源文件 这个类提供软件国际化的捷径.通过此类, ...

  5. JavaWeb国际化

    出处:https://www.cnblogs.com/plan123/p/5639803.html 1 2 根据数据的类型不同,国际化分为2类:静态数据国际化和动态数据的国际化. 静态数据,包括 &q ...

  6. java软件国际化解决方案

    Java提供给我们软件国际化的解决方案,这些国际化API基于Unicode标准,并且包括文本.(货币)数字.日期以及用户自定义对象的适配,从而使得软件能够应用到任何国家或地区.国际化英文为" ...

  7. Spring入门到放弃篇(1)- Spring国际化

    Java原生国际化 文档地址 java官方文档 参考官方文档 自定义国际化案例 public class LocaleDemo {public static void main(String[] ar ...

  8. JAVA 操作 properties 配置文件

    一.简介 Java中的properties文件是一种纯文本格式的配置文件,主要用于表达配置信息,文件类型为 *.properties,文件中内容的格式是 "键=值" 的格式.在pr ...

  9. JAVA操作properties配置文件

    一.简介 <1> java中的properties文件是一种配置文件,主要用于表达配置信息,文件类型为*.properties,格式为文本文件,文件的内容是格式是"键=值&quo ...

  10. java实现上传zip解压及判断压缩包文件夹功能

    转自:https://blog.csdn.net/qq_17025903/article/details/80408180#commentBox 直接上Service,通过代码看思路贯穿整个功能,很多 ...

最新文章

  1. Python+selenium 自动化-chrome页面静止、冻结技术,获取web动态页面的Xpath方法,查看浏览器动态dom节点
  2. tcp通讯一次最多能发送多少数据?_关于TCP/IP,必须知道的十个知识点
  3. Metasploit渗透测试框架
  4. curd什么意思中文_查英英字典:What a shame是什么意思?
  5. mysql clickhouse_通过mysql操作clickhouse
  6. Java-P:面向对象编程
  7. docker harbor 域名_docker 安装Harbor
  8. Unix环境高级编程笔记:12、高级IO
  9. php rewrite重写,yaf 自定义重写路由rewrite
  10. PowerDesigner的學習
  11. NLP --- 隐马尔可夫HMM(第一个、第二个问题解决方案)
  12. Wince Cab Manager___cab工具
  13. Mysql type字段值1改为2,2改为1
  14. 2022年低压电工考试模拟100题及模拟考试
  15. 黑马程序员前端JavaScript高级——ES6学习笔记
  16. c语言如何宏定义枚举型结构体,C语言学习笔记--枚举结构体
  17. 北京智源大会 | AI + 医疗的下一个十年:从公共卫生预警到人类基因密码破解 道翰天琼认知智能api机器人接口1。
  18. SAS9.4更新sid,有效期至2022年11月30日
  19. TCL作价5000万美元收购美国Novatel Wireless公司MIFI业务
  20. UniSwap V3协议浅析(上)

热门文章

  1. 联想台式机计算机接口,我的电脑显卡是什么样的接口?
  2. 使用 Bitmap Style Designer 为FMX修改已有样式
  3. 数理统计SPSS软件实验报告一--描述性统计
  4. 杭州电信域名解析服务器,国内电信域名解析服务器dns分布表.docx
  5. 什么索引?索引的作用是什么?索引运用实例
  6. hdu 吉哥系列故事——完美队形 (最长公共子序列)
  7. 伴随方法:线性方程的伴随方程(Adjoint Equation)
  8. 飞跃微信小程序一一新风口新模式,你所了解到的小程序有多少?
  9. Qt数据可视化(QPieSeries饼状图)
  10. 计蒜客T1006对齐输出