Revise resource bundle caching for common locales

Closes gh-36957
This commit is contained in:
Juergen Hoeller
2026-06-22 21:50:56 +02:00
parent 41b8f13e91
commit 67e5ea9509
4 changed files with 136 additions and 84 deletions
@@ -17,15 +17,19 @@
package org.springframework.context.support;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for message source implementations, providing support infrastructure
@@ -41,6 +45,9 @@ import org.springframework.util.ObjectUtils;
*/
public abstract class MessageSourceSupport {
static final Set<Locale> JVM_LOCALES = Arrays.stream(Locale.getAvailableLocales()).
filter(l -> StringUtils.hasLength(l.getLanguage())).collect(Collectors.toSet());
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
/** Logger available to subclasses. */
@@ -116,22 +123,17 @@ public abstract class MessageSourceSupport {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg;
}
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
MessageFormat messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> {
try {
return 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...
return INVALID_MESSAGE_FORMAT;
}
});
MessageFormat messageFormat;
if (locale != null && JVM_LOCALES.contains(locale)) {
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> resolveMessageFormat(msg, key));
}
else {
messageFormat = resolveMessageFormat(msg, locale);
}
if (messageFormat == INVALID_MESSAGE_FORMAT) {
return msg;
}
@@ -140,6 +142,29 @@ public abstract class MessageSourceSupport {
}
}
/**
* Resolve a {@code MessageFormat} for the given message and Locale.
* @param msg the message to create a {@code MessageFormat} for
* @param locale the Locale to create a {@code MessageFormat} for
* @return the {@code MessageFormat} instance, or otherwise
* {@link #INVALID_MESSAGE_FORMAT} if not resolvable
* @see #createMessageFormat
*/
private MessageFormat resolveMessageFormat(String msg, @Nullable Locale locale) {
try {
return 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...
return INVALID_MESSAGE_FORMAT;
}
}
/**
* Create a {@code MessageFormat} for the given message and Locale.
* @param msg the message to create a {@code MessageFormat} for
@@ -40,6 +40,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;
@@ -110,15 +111,19 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
private ResourceLoader resourceLoader = new DefaultResourceLoader();
// Cache to hold filename lists per Locale
// Cache to hold filename lists per Locale.
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
// Cache to hold already loaded properties per filename.
private final ConcurrentMap<String, PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
// Cache to hold already loaded properties per filename
// Cache to hold already merged properties per Locale.
private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>();
// Cache to hold merged properties per non-JVM Locale.
private final ConcurrentLruCache<Locale, PropertiesHolder> customLocaleProperties =
new ConcurrentLruCache<>(64, locale -> mergeProperties(collectPropertiesToMerge(locale)));
/**
* Set the list of supported file extensions.
@@ -197,20 +202,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
return propHolder.getProperty(code);
}
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;
}
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename, locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
@@ -225,20 +226,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
protected @Nullable MessageFormat resolveCode(String code, Locale locale) {
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
return propHolder.getMessageFormat(code, locale);
}
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;
}
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename, locale);
MessageFormat result = propHolder.getMessageFormat(code, locale);
if (result != null) {
return result;
}
}
}
@@ -261,12 +258,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
if (mergedHolder != null) {
return mergedHolder;
}
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
if (JVM_LOCALES.contains(locale)) {
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
if (existing != null) {
mergedHolder = existing;
}
return mergedHolder;
}
else {
return this.customLocaleProperties.get(locale);
}
return mergedHolder;
}
/**
@@ -285,7 +288,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
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);
PropertiesHolder propHolder = getProperties(filename, locale);
if (propHolder.getProperties() != null) {
holders.add(propHolder);
}
@@ -334,11 +337,11 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
// Filenames for given Locale
// Filenames for given Locale.
List<String> filenames = new ArrayList<>(7);
filenames.addAll(calculateFilenamesForLocale(basename, locale));
// Filenames for default Locale, if any
// Filenames for default Locale, if any.
Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && !defaultLocale.equals(locale)) {
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
@@ -350,24 +353,27 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
}
// Filename for default bundle file
// Filename for default bundle file.
filenames.add(basename);
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
if (JVM_LOCALES.contains(locale)) {
if (localeMap == null) {
localeMap = new ConcurrentHashMap<>();
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
if (existing != null) {
localeMap = existing;
}
}
localeMap.put(locale, filenames);
}
localeMap.put(locale, filenames);
return filenames;
}
/**
* Calculate the filenames for the given bundle basename and Locale,
* appending language code, country code, and variant code.
* <p>For example, basename "messages", Locale "de_AT_oo" &rarr; "messages_de_AT_OO",
* <p>For example, basename "messages", Locale "de_AT_OO" &rarr; "messages_de_AT_OO",
* "messages_de_AT", "messages_de".
* <p>Follows the rules defined by {@link java.util.Locale#toString()}.
* @param basename the basename of the bundle
@@ -402,6 +408,22 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
}
/**
* Get a PropertiesHolder for the given filename, either from the
* cache or freshly loaded.
* @param filename the bundle filename (basename + Locale)
* @param locale the requested locale (for cache filtering)
* @return the current PropertiesHolder for the bundle
* @see #getProperties(String)
*/
private PropertiesHolder getProperties(String filename, Locale locale) {
PropertiesHolder propHolder = getProperties(filename);
if (propHolder.getProperties() == null && !JVM_LOCALES.contains(locale)) {
this.cachedProperties.remove(filename);
}
return propHolder;
}
/**
* Get a PropertiesHolder for the given filename, either from the
* cache or freshly loaded.
@@ -188,36 +188,35 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
* found for the given basename and Locale
*/
protected @Nullable ResourceBundle getResourceBundle(String basename, Locale locale) {
if (getCacheMillis() >= 0) {
if (getCacheMillis() >= 0 || !JVM_LOCALES.contains(locale)) {
// 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.
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 = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
localeMap.put(locale, bundle);
// Cache forever: prefer local cache over repeated getBundle calls.
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
if (localeMap != null) {
ResourceBundle bundle = localeMap.get(locale);
if (bundle != null) {
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;
}
try {
ResourceBundle bundle = doGetBundle(basename, locale);
if (localeMap == null) {
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
}
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;
}
}
@@ -311,6 +310,11 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
protected @Nullable MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)
throws MissingResourceException {
if (!JVM_LOCALES.contains(locale)) {
String msg = getStringOrNull(bundle, code);
return (msg != null ? createMessageFormat(msg, locale) : null);
}
Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);
Map<Locale, MessageFormat> localeMap = null;
if (codeMap != null) {