mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Revise resource bundle caching for common locales
Closes gh-36957
This commit is contained in:
+41
-16
@@ -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
|
||||
|
||||
+67
-45
@@ -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" → "messages_de_AT_OO",
|
||||
* <p>For example, basename "messages", Locale "de_AT_OO" → "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.
|
||||
|
||||
+27
-23
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user