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 :).
Posted on September 17, 2012, in spring and tagged exceptions, locking-contention, resource-bundle. Bookmark the permalink. 2 Comments.
Right with this write-up, I truly think this site needs considerably more consideration. I’ll likely to end up again to learn much more, many thanks for that info.
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.