Issues with Spring’s ResourceBundleMessageSource

In Spring, you can use ResourceBundleMessageSource to resolve text messages from properties file, based on the selected locales. Follow this link for an introductory example on how to do so.

This post details on two bottlenecks with this message source’s implementation that we encountered through load tests.

1. This MessageSource caches both the accessed ResourceBundle instances and the generated MessageFormats for each message. The issue is that the methods to retrieve the resource bundle instances and message formats are both synchronized. This leads to locking contention when the number of users increase.

2. For every request to resolve a message, the implementation loops through all the resource bundles till it finds a resource bundle containing the key. The code to retrieve the message given the key is as below.


private String getStringOrNull(ResourceBundle bundle, String key) {
     try {
          return bundle.getString(key);
     } catch (MissingResourceException ex) {
       // Assume key not found
       // -> do NOT throw the exception to allow for checking parent message source.
       return null;
     }
}

If a given key is not found in the resource bundle (which would be the case till it searches through the right resource bundle) a MissingResourceException is thrown. Since exceptions are slow, this code becomes a cpu bottleneck when the number of users increase.

A couple of possible workarounds to the above problems are

1. Using ConcurrentHashMaps to avoid synchronizing whole methods. The getResourceBundle() method implementation would look like


private final ConcurrentHashMap<String, ConcurrentHashMap<Locale, ResourceBundle>> resourceBundles = new ConcurrentHashMap<>();

protected ResourceBundle getResourceBundle(String basename, Locale locale) {
           ConcurrentHashMap<Locale, ResourceBundle> localeMap = this.resourceBundles.get(basename);
           ResourceBundle bundle;
           if(localeMap == null) {
              ConcurrentHashMap<Locale, ResourceBundle> newLocaleMap = new ConcurrentHashMap<>();
              localeMap = resourceBundles.putIfAbsent(basename, newLocaleMap);
              if(localeMap == null) {
                 localeMap = newLocaleMap;
              }
           }

           bundle = localeMap.get(locale);
           if(bundle == null) {
              ResourceBundle newResourceBundle;
              try {
                 newResourceBundle = doGetBundle(basename, locale);
              } 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;
              }

              bundle = localeMap.putIfAbsent(locale, newResourceBundle);
              if(bundle == null) {
                 bundle = newResourceBundle;
              }
           }
           return bundle;
 }

Similarly the implementation for getMessageFormat() would look like


private final ConcurrentHashMap<ResourceBundle, ConcurrentHashMap<String, ConcurrentHashMap<Locale, MessageFormat>>> bundleMessageFormats = new ConcurrentHashMap<>();

protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) throws MissingResourceException {

          ConcurrentHashMap<String, ConcurrentHashMap<Locale, MessageFormat>> codeMap = this.bundleMessageFormats.get(bundle);

          ConcurrentHashMap<Locale, MessageFormat> localeMap = null;
          if(codeMap != null) {
             localeMap = codeMap.get(code);
             if(localeMap != null) {
                 MessageFormat result = localeMap.get(locale);
                 if(result != null) {
                   return result;
                 }
             }
          }

          String msg = getStringOrNull(bundle, code);
          if(msg != null) {
             if(codeMap == null) {
                 ConcurrentHashMap<String, ConcurrentHashMap<Locale, MessageFormat>> newCodeMap = new ConcurrentHashMap<>();
                 codeMap = this.bundleMessageFormats.putIfAbsent(bundle, newCodeMap);
                if(codeMap == null) {
                    codeMap = newCodeMap;
                }
             }

             if(localeMap == null) {
                 ConcurrentHashMap<Locale, MessageFormat> newLocaleMap = new ConcurrentHashMap<>();
                 localeMap = codeMap.putIfAbsent(code, newLocaleMap);
                 if(localeMap == null) {
                    localeMap = newLocaleMap;
                 }
             }

             MessageFormat newMessageFormat = createMessageFormat(msg, locale);
             MessageFormat result = localeMap.putIfAbsent(locale, newMessageFormat);
             if(result == null) {
                result = newMessageFormat;
             }
             return result;
        }

        return null;
 }

2. Checking if the key exists in the resource bundle before retrieving it. Hence the implementation for getStringOrNull() would look like


private String getStringOrNull(ResourceBundle bundle, String key) {
     if(bundle.containsKey(key)) {
        return bundle.getString(key);
     }
     return null;
 }

An ideal implementation would be to load all message keys from all resource bundles in one map upfront on application server start up. Something to add to the todo list :).

Advertisements

Posted on September 17, 2012, in spring and tagged , , . Bookmark the permalink. 2 Comments.

  1. Right with this write-up, I truly think this site needs considerably more consideration. Ill likely to end up again to learn much more, many thanks for that info.

  2. Nice blog article. There is an issue in Spring Jira about the performance problems in ReloadableResourceBundleMessageSource
    https://jira.springsource.org/browse/SPR-5476

    Please add your input there.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: