001/* 002 * Copyright 2002-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.test.context.cache; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.concurrent.ConcurrentHashMap; 027import java.util.concurrent.atomic.AtomicInteger; 028 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031 032import org.springframework.context.ApplicationContext; 033import org.springframework.context.ConfigurableApplicationContext; 034import org.springframework.core.style.ToStringCreator; 035import org.springframework.test.annotation.DirtiesContext.HierarchyMode; 036import org.springframework.test.context.MergedContextConfiguration; 037import org.springframework.util.Assert; 038 039/** 040 * Default implementation of the {@link ContextCache} API. 041 * 042 * <p>Uses a synchronized {@link Map} configured with a maximum size 043 * and a <em>least recently used</em> (LRU) eviction policy to cache 044 * {@link ApplicationContext} instances. 045 * 046 * <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int) 047 * constructor argument} or set via a system property or Spring property named 048 * {@code spring.test.context.cache.maxSize}. 049 * 050 * @author Sam Brannen 051 * @author Juergen Hoeller 052 * @since 2.5 053 * @see ContextCacheUtils#retrieveMaxCacheSize() 054 */ 055public class DefaultContextCache implements ContextCache { 056 057 private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY); 058 059 /** 060 * Map of context keys to Spring {@code ApplicationContext} instances. 061 */ 062 private final Map<MergedContextConfiguration, ApplicationContext> contextMap = 063 Collections.synchronizedMap(new LruCache(32, 0.75f)); 064 065 /** 066 * Map of parent keys to sets of children keys, representing a top-down <em>tree</em> 067 * of context hierarchies. This information is used for determining which subtrees 068 * need to be recursively removed and closed when removing a context that is a parent 069 * of other contexts. 070 */ 071 private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = 072 new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(32); 073 074 private final int maxSize; 075 076 private final AtomicInteger hitCount = new AtomicInteger(); 077 078 private final AtomicInteger missCount = new AtomicInteger(); 079 080 081 /** 082 * Create a new {@code DefaultContextCache} using the maximum cache size 083 * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}. 084 * @since 4.3 085 * @see #DefaultContextCache(int) 086 * @see ContextCacheUtils#retrieveMaxCacheSize() 087 */ 088 public DefaultContextCache() { 089 this(ContextCacheUtils.retrieveMaxCacheSize()); 090 } 091 092 /** 093 * Create a new {@code DefaultContextCache} using the supplied maximum 094 * cache size. 095 * @param maxSize the maximum cache size 096 * @throws IllegalArgumentException if the supplied {@code maxSize} value 097 * is not positive 098 * @since 4.3 099 * @see #DefaultContextCache() 100 */ 101 public DefaultContextCache(int maxSize) { 102 Assert.isTrue(maxSize > 0, "'maxSize' must be positive"); 103 this.maxSize = maxSize; 104 } 105 106 107 /** 108 * {@inheritDoc} 109 */ 110 @Override 111 public boolean contains(MergedContextConfiguration key) { 112 Assert.notNull(key, "Key must not be null"); 113 return this.contextMap.containsKey(key); 114 } 115 116 /** 117 * {@inheritDoc} 118 */ 119 @Override 120 public ApplicationContext get(MergedContextConfiguration key) { 121 Assert.notNull(key, "Key must not be null"); 122 ApplicationContext context = this.contextMap.get(key); 123 if (context == null) { 124 this.missCount.incrementAndGet(); 125 } 126 else { 127 this.hitCount.incrementAndGet(); 128 } 129 return context; 130 } 131 132 /** 133 * {@inheritDoc} 134 */ 135 @Override 136 public void put(MergedContextConfiguration key, ApplicationContext context) { 137 Assert.notNull(key, "Key must not be null"); 138 Assert.notNull(context, "ApplicationContext must not be null"); 139 140 this.contextMap.put(key, context); 141 MergedContextConfiguration child = key; 142 MergedContextConfiguration parent = child.getParent(); 143 while (parent != null) { 144 Set<MergedContextConfiguration> list = this.hierarchyMap.get(parent); 145 if (list == null) { 146 list = new HashSet<MergedContextConfiguration>(); 147 this.hierarchyMap.put(parent, list); 148 } 149 list.add(child); 150 child = parent; 151 parent = child.getParent(); 152 } 153 } 154 155 /** 156 * {@inheritDoc} 157 */ 158 @Override 159 public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { 160 Assert.notNull(key, "Key must not be null"); 161 162 // startKey is the level at which to begin clearing the cache, depending 163 // on the configured hierarchy mode. 164 MergedContextConfiguration startKey = key; 165 if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { 166 while (startKey.getParent() != null) { 167 startKey = startKey.getParent(); 168 } 169 } 170 171 List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>(); 172 remove(removedContexts, startKey); 173 174 // Remove all remaining references to any removed contexts from the 175 // hierarchy map. 176 for (MergedContextConfiguration currentKey : removedContexts) { 177 for (Set<MergedContextConfiguration> children : this.hierarchyMap.values()) { 178 children.remove(currentKey); 179 } 180 } 181 182 // Remove empty entries from the hierarchy map. 183 for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { 184 if (this.hierarchyMap.get(currentKey).isEmpty()) { 185 this.hierarchyMap.remove(currentKey); 186 } 187 } 188 } 189 190 private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) { 191 Assert.notNull(key, "Key must not be null"); 192 193 Set<MergedContextConfiguration> children = this.hierarchyMap.get(key); 194 if (children != null) { 195 for (MergedContextConfiguration child : children) { 196 // Recurse through lower levels 197 remove(removedContexts, child); 198 } 199 // Remove the set of children for the current context from the hierarchy map. 200 this.hierarchyMap.remove(key); 201 } 202 203 // Physically remove and close leaf nodes first (i.e., on the way back up the 204 // stack as opposed to prior to the recursive call). 205 ApplicationContext context = this.contextMap.remove(key); 206 if (context instanceof ConfigurableApplicationContext) { 207 ((ConfigurableApplicationContext) context).close(); 208 } 209 removedContexts.add(key); 210 } 211 212 /** 213 * {@inheritDoc} 214 */ 215 @Override 216 public int size() { 217 return this.contextMap.size(); 218 } 219 220 /** 221 * Get the maximum size of this cache. 222 */ 223 public int getMaxSize() { 224 return this.maxSize; 225 } 226 227 /** 228 * {@inheritDoc} 229 */ 230 @Override 231 public int getParentContextCount() { 232 return this.hierarchyMap.size(); 233 } 234 235 /** 236 * {@inheritDoc} 237 */ 238 @Override 239 public int getHitCount() { 240 return this.hitCount.get(); 241 } 242 243 /** 244 * {@inheritDoc} 245 */ 246 @Override 247 public int getMissCount() { 248 return this.missCount.get(); 249 } 250 251 /** 252 * {@inheritDoc} 253 */ 254 @Override 255 public void reset() { 256 synchronized (this.contextMap) { 257 clear(); 258 clearStatistics(); 259 } 260 } 261 262 /** 263 * {@inheritDoc} 264 */ 265 @Override 266 public void clear() { 267 synchronized (this.contextMap) { 268 this.contextMap.clear(); 269 this.hierarchyMap.clear(); 270 } 271 } 272 273 /** 274 * {@inheritDoc} 275 */ 276 @Override 277 public void clearStatistics() { 278 synchronized (this.contextMap) { 279 this.hitCount.set(0); 280 this.missCount.set(0); 281 } 282 } 283 284 /** 285 * {@inheritDoc} 286 */ 287 @Override 288 public void logStatistics() { 289 if (statsLogger.isDebugEnabled()) { 290 statsLogger.debug("Spring test ApplicationContext cache statistics: " + this); 291 } 292 } 293 294 /** 295 * Generate a text string containing the implementation type of this 296 * cache and its statistics. 297 * <p>The string returned by this method contains all information 298 * required for compliance with the contract for {@link #logStatistics()}. 299 * @return a string representation of this cache, including statistics 300 */ 301 @Override 302 public String toString() { 303 return new ToStringCreator(this) 304 .append("size", size()) 305 .append("maxSize", getMaxSize()) 306 .append("parentContextCount", getParentContextCount()) 307 .append("hitCount", getHitCount()) 308 .append("missCount", getMissCount()) 309 .toString(); 310 } 311 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 * @since 4.3 318 */ 319 @SuppressWarnings("serial") 320 private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> { 321 322 /** 323 * Create a new {@code LruCache} with the supplied initial capacity 324 * and load factor. 325 * @param initialCapacity the initial capacity 326 * @param loadFactor the load factor 327 */ 328 LruCache(int initialCapacity, float loadFactor) { 329 super(initialCapacity, loadFactor, true); 330 } 331 332 @Override 333 protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) { 334 if (this.size() > DefaultContextCache.this.getMaxSize()) { 335 // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally 336 // invoke java.util.Map.remove(Object, Object). 337 DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL); 338 } 339 340 // Return false since we invoke a custom eviction algorithm. 341 return false; 342 } 343 } 344 345}