Skip to content

Commit a000dd7

Browse files
committed
ReloadableResourceBundleMessageSource uses ConcurrentHashMaps and ReentrantLocks instead of synchronization
Issue: SPR-10500
1 parent f5cf3cd commit a000dd7

File tree

2 files changed

+204
-111
lines changed

2 files changed

+204
-111
lines changed

spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java

Lines changed: 139 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,11 +21,13 @@
2121
import java.io.InputStreamReader;
2222
import java.text.MessageFormat;
2323
import java.util.ArrayList;
24-
import java.util.HashMap;
2524
import java.util.List;
2625
import java.util.Locale;
2726
import java.util.Map;
2827
import java.util.Properties;
28+
import java.util.concurrent.ConcurrentHashMap;
29+
import java.util.concurrent.ConcurrentMap;
30+
import java.util.concurrent.locks.ReentrantLock;
2931

3032
import org.springframework.context.ResourceLoaderAware;
3133
import org.springframework.core.io.DefaultResourceLoader;
@@ -92,8 +94,7 @@
9294
* @see ResourceBundleMessageSource
9395
* @see java.util.ResourceBundle
9496
*/
95-
public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
96-
implements ResourceLoaderAware {
97+
public class ReloadableResourceBundleMessageSource extends AbstractMessageSource implements ResourceLoaderAware {
9798

9899
private static final String PROPERTIES_SUFFIX = ".properties";
99100

@@ -110,19 +111,23 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
110111

111112
private long cacheMillis = -1;
112113

114+
private boolean concurrentRefresh = true;
115+
113116
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
114117

115118
private ResourceLoader resourceLoader = new DefaultResourceLoader();
116119

117120
/** Cache to hold filename lists per Locale */
118-
private final Map<String, Map<Locale, List<String>>> cachedFilenames =
119-
new HashMap<String, Map<Locale, List<String>>>();
121+
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames =
122+
new ConcurrentHashMap<String, Map<Locale, List<String>>>();
120123

121124
/** Cache to hold already loaded properties per filename */
122-
private final Map<String, PropertiesHolder> cachedProperties = new HashMap<String, PropertiesHolder>();
125+
private final ConcurrentMap<String, PropertiesHolder> cachedProperties =
126+
new ConcurrentHashMap<String, PropertiesHolder>();
123127

124128
/** Cache to hold merged loaded properties per locale */
125-
private final Map<Locale, PropertiesHolder> cachedMergedProperties = new HashMap<Locale, PropertiesHolder>();
129+
private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties =
130+
new ConcurrentHashMap<Locale, PropertiesHolder>();
126131

127132

128133
/**
@@ -231,6 +236,20 @@ public void setCacheSeconds(int cacheSeconds) {
231236
this.cacheMillis = (cacheSeconds * 1000);
232237
}
233238

239+
/**
240+
* Specify whether to allow for concurrent refresh behavior, i.e. one thread
241+
* locked in a refresh attempt for a specific cached properties file whereas
242+
* other threads keep returning the old properties for the time being, until
243+
* the refresh attempt has completed.
244+
* <p>Default is "true": This behavior is new as of Spring Framework 4.1,
245+
* minimizing contention between threads. If you prefer the old behavior,
246+
* i.e. to fully block on refresh, switch this flag to "false".
247+
* @see #setCacheSeconds
248+
*/
249+
public void setConcurrentRefresh(boolean concurrentRefresh) {
250+
this.concurrentRefresh = concurrentRefresh;
251+
}
252+
234253
/**
235254
* Set the PropertiesPersister to use for parsing properties files.
236255
* <p>The default is a DefaultPropertiesPersister.
@@ -322,26 +341,27 @@ protected MessageFormat resolveCode(String code, Locale locale) {
322341
* cached forever.
323342
*/
324343
protected PropertiesHolder getMergedProperties(Locale locale) {
325-
synchronized (this.cachedMergedProperties) {
326-
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
327-
if (mergedHolder != null) {
328-
return mergedHolder;
329-
}
330-
Properties mergedProps = new Properties();
331-
mergedHolder = new PropertiesHolder(mergedProps, -1);
332-
for (int i = this.basenames.length - 1; i >= 0; i--) {
333-
List<String> filenames = calculateAllFilenames(this.basenames[i], locale);
334-
for (int j = filenames.size() - 1; j >= 0; j--) {
335-
String filename = filenames.get(j);
336-
PropertiesHolder propHolder = getProperties(filename);
337-
if (propHolder.getProperties() != null) {
338-
mergedProps.putAll(propHolder.getProperties());
339-
}
344+
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
345+
if (mergedHolder != null) {
346+
return mergedHolder;
347+
}
348+
Properties mergedProps = new Properties();
349+
mergedHolder = new PropertiesHolder(mergedProps, -1);
350+
for (int i = this.basenames.length - 1; i >= 0; i--) {
351+
List<String> filenames = calculateAllFilenames(this.basenames[i], locale);
352+
for (int j = filenames.size() - 1; j >= 0; j--) {
353+
String filename = filenames.get(j);
354+
PropertiesHolder propHolder = getProperties(filename);
355+
if (propHolder.getProperties() != null) {
356+
mergedProps.putAll(propHolder.getProperties());
340357
}
341358
}
342-
this.cachedMergedProperties.put(locale, mergedHolder);
343-
return mergedHolder;
344359
}
360+
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
361+
if (existing != null) {
362+
mergedHolder = existing;
363+
}
364+
return mergedHolder;
345365
}
346366

347367
/**
@@ -355,36 +375,34 @@ protected PropertiesHolder getMergedProperties(Locale locale) {
355375
* @see #calculateFilenamesForLocale
356376
*/
357377
protected List<String> calculateAllFilenames(String basename, Locale locale) {
358-
synchronized (this.cachedFilenames) {
359-
Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
360-
if (localeMap != null) {
361-
List<String> filenames = localeMap.get(locale);
362-
if (filenames != null) {
363-
return filenames;
364-
}
378+
Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
379+
if (localeMap != null) {
380+
List<String> filenames = localeMap.get(locale);
381+
if (filenames != null) {
382+
return filenames;
365383
}
366-
List<String> filenames = new ArrayList<String>(7);
367-
filenames.addAll(calculateFilenamesForLocale(basename, locale));
368-
if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {
369-
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
370-
for (String fallbackFilename : fallbackFilenames) {
371-
if (!filenames.contains(fallbackFilename)) {
372-
// Entry for fallback locale that isn't already in filenames list.
373-
filenames.add(fallbackFilename);
374-
}
384+
}
385+
List<String> filenames = new ArrayList<String>(7);
386+
filenames.addAll(calculateFilenamesForLocale(basename, locale));
387+
if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) {
388+
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, Locale.getDefault());
389+
for (String fallbackFilename : fallbackFilenames) {
390+
if (!filenames.contains(fallbackFilename)) {
391+
// Entry for fallback locale that isn't already in filenames list.
392+
filenames.add(fallbackFilename);
375393
}
376394
}
377-
filenames.add(basename);
378-
if (localeMap != null) {
379-
localeMap.put(locale, filenames);
380-
}
381-
else {
382-
localeMap = new HashMap<Locale, List<String>>();
383-
localeMap.put(locale, filenames);
384-
this.cachedFilenames.put(basename, localeMap);
395+
}
396+
filenames.add(basename);
397+
if (localeMap == null) {
398+
localeMap = new ConcurrentHashMap<Locale, List<String>>();
399+
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
400+
if (existing != null) {
401+
localeMap = existing;
385402
}
386-
return filenames;
387403
}
404+
localeMap.put(locale, filenames);
405+
return filenames;
388406
}
389407

390408
/**
@@ -432,16 +450,46 @@ protected List<String> calculateFilenamesForLocale(String basename, Locale local
432450
* @return the current PropertiesHolder for the bundle
433451
*/
434452
protected PropertiesHolder getProperties(String filename) {
435-
synchronized (this.cachedProperties) {
436-
PropertiesHolder propHolder = this.cachedProperties.get(filename);
437-
if (propHolder != null &&
438-
(propHolder.getRefreshTimestamp() < 0 ||
439-
propHolder.getRefreshTimestamp() > System.currentTimeMillis() - this.cacheMillis)) {
440-
// up to date
453+
PropertiesHolder propHolder = this.cachedProperties.get(filename);
454+
long originalTimestamp = -1;
455+
456+
if (propHolder != null) {
457+
originalTimestamp = propHolder.getRefreshTimestamp();
458+
if (originalTimestamp < 0 || originalTimestamp > System.currentTimeMillis() - this.cacheMillis) {
459+
// Up to date
460+
return propHolder;
461+
}
462+
}
463+
else {
464+
propHolder = new PropertiesHolder();
465+
PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(filename, propHolder);
466+
if (existingHolder != null) {
467+
propHolder = existingHolder;
468+
}
469+
}
470+
471+
// At this point, we need to refresh...
472+
if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
473+
// A populated but stale holder -> could keep using it.
474+
if (!propHolder.refreshLock.tryLock()) {
475+
// Getting refreshed by another thread already ->
476+
// let's return the existing properties for the time being.
441477
return propHolder;
442478
}
479+
}
480+
else {
481+
propHolder.refreshLock.lock();
482+
}
483+
try {
484+
PropertiesHolder existingHolder = this.cachedProperties.get(filename);
485+
if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
486+
return existingHolder;
487+
}
443488
return refreshProperties(filename, propHolder);
444489
}
490+
finally {
491+
propHolder.refreshLock.unlock();
492+
}
445493
}
446494

447495
/**
@@ -476,8 +524,7 @@ protected PropertiesHolder refreshProperties(String filename, PropertiesHolder p
476524
catch (IOException ex) {
477525
// Probably a class path resource: cache it forever.
478526
if (logger.isDebugEnabled()) {
479-
logger.debug(
480-
resource + " could not be resolved in the file system - assuming that is hasn't changed", ex);
527+
logger.debug(resource + " could not be resolved in the file system - assuming that it hasn't changed", ex);
481528
}
482529
fileTimestamp = -1;
483530
}
@@ -561,12 +608,8 @@ protected Properties loadProperties(Resource resource, String filename) throws I
561608
*/
562609
public void clearCache() {
563610
logger.debug("Clearing entire resource bundle cache");
564-
synchronized (this.cachedProperties) {
565-
this.cachedProperties.clear();
566-
}
567-
synchronized (this.cachedMergedProperties) {
568-
this.cachedMergedProperties.clear();
569-
}
611+
this.cachedProperties.clear();
612+
this.cachedMergedProperties.clear();
570613
}
571614

572615
/**
@@ -595,38 +638,42 @@ public String toString() {
595638
*/
596639
protected class PropertiesHolder {
597640

598-
private Properties properties;
641+
private final Properties properties;
599642

600-
private long fileTimestamp = -1;
643+
private final long fileTimestamp;
601644

602-
private long refreshTimestamp = -1;
645+
private volatile long refreshTimestamp = -2;
646+
647+
private final ReentrantLock refreshLock = new ReentrantLock();
603648

604649
/** Cache to hold already generated MessageFormats per message code */
605-
private final Map<String, Map<Locale, MessageFormat>> cachedMessageFormats =
606-
new HashMap<String, Map<Locale, MessageFormat>>();
650+
private final ConcurrentMap<String, Map<Locale, MessageFormat>> cachedMessageFormats =
651+
new ConcurrentHashMap<String, Map<Locale, MessageFormat>>();
652+
653+
public PropertiesHolder() {
654+
this.properties = null;
655+
this.fileTimestamp = -1;
656+
}
607657

608658
public PropertiesHolder(Properties properties, long fileTimestamp) {
609659
this.properties = properties;
610660
this.fileTimestamp = fileTimestamp;
611661
}
612662

613-
public PropertiesHolder() {
614-
}
615-
616663
public Properties getProperties() {
617-
return properties;
664+
return this.properties;
618665
}
619666

620667
public long getFileTimestamp() {
621-
return fileTimestamp;
668+
return this.fileTimestamp;
622669
}
623670

624671
public void setRefreshTimestamp(long refreshTimestamp) {
625672
this.refreshTimestamp = refreshTimestamp;
626673
}
627674

628675
public long getRefreshTimestamp() {
629-
return refreshTimestamp;
676+
return this.refreshTimestamp;
630677
}
631678

632679
public String getProperty(String code) {
@@ -640,26 +687,27 @@ public MessageFormat getMessageFormat(String code, Locale locale) {
640687
if (this.properties == null) {
641688
return null;
642689
}
643-
synchronized (this.cachedMessageFormats) {
644-
Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
645-
if (localeMap != null) {
646-
MessageFormat result = localeMap.get(locale);
647-
if (result != null) {
648-
return result;
649-
}
690+
Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
691+
if (localeMap != null) {
692+
MessageFormat result = localeMap.get(locale);
693+
if (result != null) {
694+
return result;
650695
}
651-
String msg = this.properties.getProperty(code);
652-
if (msg != null) {
653-
if (localeMap == null) {
654-
localeMap = new HashMap<Locale, MessageFormat>();
655-
this.cachedMessageFormats.put(code, localeMap);
696+
}
697+
String msg = this.properties.getProperty(code);
698+
if (msg != null) {
699+
if (localeMap == null) {
700+
localeMap = new ConcurrentHashMap<Locale, MessageFormat>();
701+
Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
702+
if (existing != null) {
703+
localeMap = existing;
656704
}
657-
MessageFormat result = createMessageFormat(msg, locale);
658-
localeMap.put(locale, result);
659-
return result;
660705
}
661-
return null;
706+
MessageFormat result = createMessageFormat(msg, locale);
707+
localeMap.put(locale, result);
708+
return result;
662709
}
710+
return null;
663711
}
664712
}
665713

0 commit comments

Comments
 (0)