11/*
2- * Copyright 2002-2015 the original author or authors.
2+ * Copyright 2002-2016 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.
1717package org .springframework .test .context .cache ;
1818
1919import java .util .ArrayList ;
20+ import java .util .Collections ;
2021import java .util .HashSet ;
22+ import java .util .LinkedHashMap ;
2123import java .util .List ;
2224import java .util .Map ;
2325import java .util .Set ;
3739/**
3840 * Default implementation of the {@link ContextCache} API.
3941 *
40- * <p>Uses {@link ConcurrentHashMap ConcurrentHashMaps} to cache
41- * {@link ApplicationContext} and {@link MergedContextConfiguration} instances.
42+ * <p>Uses a synchronized {@link Map} configured with a maximum size
43+ * and a <em>least recently used</em> (LRU) eviction policy to cache
44+ * {@link ApplicationContext} instances.
45+ *
46+ * <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int)
47+ * constructor argument} or set via a system property or Spring property named
48+ * {@code spring.test.context.cache.maxSize}.
4249 *
4350 * @author Sam Brannen
4451 * @author Juergen Hoeller
4552 * @since 2.5
53+ * @see ContextCacheUtils#retrieveMaxCacheSize()
4654 */
4755public class DefaultContextCache implements ContextCache {
4856
@@ -52,7 +60,7 @@ public class DefaultContextCache implements ContextCache {
5260 * Map of context keys to Spring {@code ApplicationContext} instances.
5361 */
5462 private final Map <MergedContextConfiguration , ApplicationContext > contextMap =
55- new ConcurrentHashMap < MergedContextConfiguration , ApplicationContext >( 64 );
63+ Collections . synchronizedMap ( new LruCache ( 32 , 0.75f ) );
5664
5765 /**
5866 * Map of parent keys to sets of children keys, representing a top-down <em>tree</em>
@@ -61,13 +69,41 @@ public class DefaultContextCache implements ContextCache {
6169 * of other contexts.
6270 */
6371 private final Map <MergedContextConfiguration , Set <MergedContextConfiguration >> hierarchyMap =
64- new ConcurrentHashMap <MergedContextConfiguration , Set <MergedContextConfiguration >>(64 );
72+ new ConcurrentHashMap <MergedContextConfiguration , Set <MergedContextConfiguration >>(32 );
73+
74+ private final int maxSize ;
6575
6676 private final AtomicInteger hitCount = new AtomicInteger ();
6777
6878 private final AtomicInteger missCount = new AtomicInteger ();
6979
7080
81+ /**
82+ * Create a new {@code DefaultContextCache} using the maximum cache size
83+ * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}.
84+ * @since 4.3
85+ * @see #DefaultContextCache(int)
86+ * @see ContextCacheUtils#retrieveMaxCacheSize()
87+ */
88+ public DefaultContextCache () {
89+ this (ContextCacheUtils .retrieveMaxCacheSize ());
90+ }
91+
92+ /**
93+ * Create a new {@code DefaultContextCache} using the supplied maximum
94+ * cache size.
95+ * @param maxSize the maximum cache size
96+ * @throws IllegalArgumentException if the supplied {@code maxSize} value
97+ * is not positive
98+ * @since 4.3
99+ * @see #DefaultContextCache()
100+ */
101+ public DefaultContextCache (int maxSize ) {
102+ Assert .isTrue (maxSize > 0 , "maxSize must be positive" );
103+ this .maxSize = maxSize ;
104+ }
105+
106+
71107 /**
72108 * {@inheritDoc}
73109 */
@@ -181,6 +217,13 @@ public int size() {
181217 return this .contextMap .size ();
182218 }
183219
220+ /**
221+ * Get the maximum size of this cache.
222+ */
223+ public int getMaxSize () {
224+ return this .maxSize ;
225+ }
226+
184227 /**
185228 * {@inheritDoc}
186229 */
@@ -210,7 +253,7 @@ public int getMissCount() {
210253 */
211254 @ Override
212255 public void reset () {
213- synchronized (contextMap ) {
256+ synchronized (this . contextMap ) {
214257 clear ();
215258 clearStatistics ();
216259 }
@@ -221,7 +264,7 @@ public void reset() {
221264 */
222265 @ Override
223266 public void clear () {
224- synchronized (contextMap ) {
267+ synchronized (this . contextMap ) {
225268 this .contextMap .clear ();
226269 this .hierarchyMap .clear ();
227270 }
@@ -232,7 +275,7 @@ public void clear() {
232275 */
233276 @ Override
234277 public void clearStatistics () {
235- synchronized (contextMap ) {
278+ synchronized (this . contextMap ) {
236279 this .hitCount .set (0 );
237280 this .missCount .set (0 );
238281 }
@@ -259,10 +302,46 @@ public void logStatistics() {
259302 public String toString () {
260303 return new ToStringCreator (this )
261304 .append ("size" , size ())
305+ .append ("maxSize" , getMaxSize ())
262306 .append ("parentContextCount" , getParentContextCount ())
263307 .append ("hitCount" , getHitCount ())
264308 .append ("missCount" , getMissCount ())
265309 .toString ();
266310 }
267311
312+
313+ /**
314+ * Simple cache implementation based on {@link LinkedHashMap} with a maximum
315+ * size and a <em>least recently used</em> (LRU) eviction policy that
316+ * properly closes application contexts.
317+ *
318+ * @author Sam Brannen
319+ * @since 4.3
320+ */
321+ @ SuppressWarnings ("serial" )
322+ private class LruCache extends LinkedHashMap <MergedContextConfiguration , ApplicationContext > {
323+
324+ /**
325+ * Create a new {@code LruCache} with the supplied initial capacity and
326+ * load factor.
327+ * @param initialCapacity the initial capacity
328+ * @param loadFactor the load factor
329+ */
330+ LruCache (int initialCapacity , float loadFactor ) {
331+ super (initialCapacity , loadFactor , true );
332+ }
333+
334+ @ Override
335+ protected boolean removeEldestEntry (Map .Entry <MergedContextConfiguration , ApplicationContext > eldest ) {
336+ if (this .size () > DefaultContextCache .this .getMaxSize ()) {
337+ // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally
338+ // invoke java.util.Map.remove(Object, Object).
339+ DefaultContextCache .this .remove (eldest .getKey (), HierarchyMode .CURRENT_LEVEL );
340+ }
341+
342+ // Return false since we invoke a custom eviction algorithm.
343+ return false ;
344+ }
345+ }
346+
268347}
0 commit comments