1
1
/*
2
- * Copyright 2002-2012 the original author or authors.
2
+ * Copyright 2002-2014 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
21
21
import java .io .InputStreamReader ;
22
22
import java .text .MessageFormat ;
23
23
import java .util .ArrayList ;
24
- import java .util .HashMap ;
25
24
import java .util .List ;
26
25
import java .util .Locale ;
27
26
import java .util .Map ;
28
27
import java .util .Properties ;
28
+ import java .util .concurrent .ConcurrentHashMap ;
29
+ import java .util .concurrent .ConcurrentMap ;
30
+ import java .util .concurrent .locks .ReentrantLock ;
29
31
30
32
import org .springframework .context .ResourceLoaderAware ;
31
33
import org .springframework .core .io .DefaultResourceLoader ;
92
94
* @see ResourceBundleMessageSource
93
95
* @see java.util.ResourceBundle
94
96
*/
95
- public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
96
- implements ResourceLoaderAware {
97
+ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource implements ResourceLoaderAware {
97
98
98
99
private static final String PROPERTIES_SUFFIX = ".properties" ;
99
100
@@ -110,19 +111,23 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
110
111
111
112
private long cacheMillis = -1 ;
112
113
114
+ private boolean concurrentRefresh = true ;
115
+
113
116
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister ();
114
117
115
118
private ResourceLoader resourceLoader = new DefaultResourceLoader ();
116
119
117
120
/** 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 >>>();
120
123
121
124
/** 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 >();
123
127
124
128
/** 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 >();
126
131
127
132
128
133
/**
@@ -231,6 +236,20 @@ public void setCacheSeconds(int cacheSeconds) {
231
236
this .cacheMillis = (cacheSeconds * 1000 );
232
237
}
233
238
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
+
234
253
/**
235
254
* Set the PropertiesPersister to use for parsing properties files.
236
255
* <p>The default is a DefaultPropertiesPersister.
@@ -322,26 +341,27 @@ protected MessageFormat resolveCode(String code, Locale locale) {
322
341
* cached forever.
323
342
*/
324
343
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 ());
340
357
}
341
358
}
342
- this .cachedMergedProperties .put (locale , mergedHolder );
343
- return mergedHolder ;
344
359
}
360
+ PropertiesHolder existing = this .cachedMergedProperties .putIfAbsent (locale , mergedHolder );
361
+ if (existing != null ) {
362
+ mergedHolder = existing ;
363
+ }
364
+ return mergedHolder ;
345
365
}
346
366
347
367
/**
@@ -355,36 +375,34 @@ protected PropertiesHolder getMergedProperties(Locale locale) {
355
375
* @see #calculateFilenamesForLocale
356
376
*/
357
377
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 ;
365
383
}
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 );
375
393
}
376
394
}
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 ;
385
402
}
386
- return filenames ;
387
403
}
404
+ localeMap .put (locale , filenames );
405
+ return filenames ;
388
406
}
389
407
390
408
/**
@@ -432,16 +450,46 @@ protected List<String> calculateFilenamesForLocale(String basename, Locale local
432
450
* @return the current PropertiesHolder for the bundle
433
451
*/
434
452
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.
441
477
return propHolder ;
442
478
}
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
+ }
443
488
return refreshProperties (filename , propHolder );
444
489
}
490
+ finally {
491
+ propHolder .refreshLock .unlock ();
492
+ }
445
493
}
446
494
447
495
/**
@@ -476,8 +524,7 @@ protected PropertiesHolder refreshProperties(String filename, PropertiesHolder p
476
524
catch (IOException ex ) {
477
525
// Probably a class path resource: cache it forever.
478
526
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 );
481
528
}
482
529
fileTimestamp = -1 ;
483
530
}
@@ -561,12 +608,8 @@ protected Properties loadProperties(Resource resource, String filename) throws I
561
608
*/
562
609
public void clearCache () {
563
610
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 ();
570
613
}
571
614
572
615
/**
@@ -595,38 +638,42 @@ public String toString() {
595
638
*/
596
639
protected class PropertiesHolder {
597
640
598
- private Properties properties ;
641
+ private final Properties properties ;
599
642
600
- private long fileTimestamp = - 1 ;
643
+ private final long fileTimestamp ;
601
644
602
- private long refreshTimestamp = -1 ;
645
+ private volatile long refreshTimestamp = -2 ;
646
+
647
+ private final ReentrantLock refreshLock = new ReentrantLock ();
603
648
604
649
/** 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
+ }
607
657
608
658
public PropertiesHolder (Properties properties , long fileTimestamp ) {
609
659
this .properties = properties ;
610
660
this .fileTimestamp = fileTimestamp ;
611
661
}
612
662
613
- public PropertiesHolder () {
614
- }
615
-
616
663
public Properties getProperties () {
617
- return properties ;
664
+ return this . properties ;
618
665
}
619
666
620
667
public long getFileTimestamp () {
621
- return fileTimestamp ;
668
+ return this . fileTimestamp ;
622
669
}
623
670
624
671
public void setRefreshTimestamp (long refreshTimestamp ) {
625
672
this .refreshTimestamp = refreshTimestamp ;
626
673
}
627
674
628
675
public long getRefreshTimestamp () {
629
- return refreshTimestamp ;
676
+ return this . refreshTimestamp ;
630
677
}
631
678
632
679
public String getProperty (String code ) {
@@ -640,26 +687,27 @@ public MessageFormat getMessageFormat(String code, Locale locale) {
640
687
if (this .properties == null ) {
641
688
return null ;
642
689
}
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 ;
650
695
}
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 ;
656
704
}
657
- MessageFormat result = createMessageFormat (msg , locale );
658
- localeMap .put (locale , result );
659
- return result ;
660
705
}
661
- return null ;
706
+ MessageFormat result = createMessageFormat (msg , locale );
707
+ localeMap .put (locale , result );
708
+ return result ;
662
709
}
710
+ return null ;
663
711
}
664
712
}
665
713
0 commit comments