001/* 002 * Copyright 2002-2019 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.lang.Nullable; 036import org.springframework.test.annotation.DirtiesContext.HierarchyMode; 037import org.springframework.test.context.MergedContextConfiguration; 038import org.springframework.util.Assert; 039 040/** 041 * Default implementation of the {@link ContextCache} API. 042 * 043 * <p>Uses a synchronized {@link Map} configured with a maximum size 044 * and a <em>least recently used</em> (LRU) eviction policy to cache 045 * {@link ApplicationContext} instances. 046 * 047 * <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int) 048 * constructor argument} or set via a system property or Spring property named 049 * {@code spring.test.context.cache.maxSize}. 050 * 051 * @author Sam Brannen 052 * @author Juergen Hoeller 053 * @since 2.5 054 * @see ContextCacheUtils#retrieveMaxCacheSize() 055 */ 056public class DefaultContextCache implements ContextCache { 057 058 private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY); 059 060 /** 061 * Map of context keys to Spring {@code ApplicationContext} instances. 062 */ 063 private final Map<MergedContextConfiguration, ApplicationContext> contextMap = 064 Collections.synchronizedMap(new LruCache(32, 0.75f)); 065 066 /** 067 * Map of parent keys to sets of children keys, representing a top-down <em>tree</em> 068 * of context hierarchies. This information is used for determining which subtrees 069 * need to be recursively removed and closed when removing a context that is a parent 070 * of other contexts. 071 */ 072 private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = 073 new ConcurrentHashMap<>(32); 074 075 private final int maxSize; 076 077 private final AtomicInteger hitCount = new AtomicInteger(); 078 079 private final AtomicInteger missCount = new AtomicInteger(); 080 081 082 /** 083 * Create a new {@code DefaultContextCache} using the maximum cache size 084 * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}. 085 * @since 4.3 086 * @see #DefaultContextCache(int) 087 * @see ContextCacheUtils#retrieveMaxCacheSize() 088 */ 089 public DefaultContextCache() { 090 this(ContextCacheUtils.retrieveMaxCacheSize()); 091 } 092 093 /** 094 * Create a new {@code DefaultContextCache} using the supplied maximum 095 * cache size. 096 * @param maxSize the maximum cache size 097 * @throws IllegalArgumentException if the supplied {@code maxSize} value 098 * is not positive 099 * @since 4.3 100 * @see #DefaultContextCache() 101 */ 102 public DefaultContextCache(int maxSize) { 103 Assert.isTrue(maxSize > 0, "'maxSize' must be positive"); 104 this.maxSize = maxSize; 105 } 106 107 108 /** 109 * {@inheritDoc} 110 */ 111 @Override 112 public boolean contains(MergedContextConfiguration key) { 113 Assert.notNull(key, "Key must not be null"); 114 return this.contextMap.containsKey(key); 115 } 116 117 /** 118 * {@inheritDoc} 119 */ 120 @Override 121 @Nullable 122 public ApplicationContext get(MergedContextConfiguration key) { 123 Assert.notNull(key, "Key must not be null"); 124 ApplicationContext context = this.contextMap.get(key); 125 if (context == null) { 126 this.missCount.incrementAndGet(); 127 } 128 else { 129 this.hitCount.incrementAndGet(); 130 } 131 return context; 132 } 133 134 /** 135 * {@inheritDoc} 136 */ 137 @Override 138 public void put(MergedContextConfiguration key, ApplicationContext context) { 139 Assert.notNull(key, "Key must not be null"); 140 Assert.notNull(context, "ApplicationContext must not be null"); 141 142 this.contextMap.put(key, context); 143 MergedContextConfiguration child = key; 144 MergedContextConfiguration parent = child.getParent(); 145 while (parent != null) { 146 Set<MergedContextConfiguration> list = this.hierarchyMap.computeIfAbsent(parent, k -> new HashSet<>()); 147 list.add(child); 148 child = parent; 149 parent = child.getParent(); 150 } 151 } 152 153 /** 154 * {@inheritDoc} 155 */ 156 @Override 157 public void remove(MergedContextConfiguration key, @Nullable HierarchyMode hierarchyMode) { 158 Assert.notNull(key, "Key must not be null"); 159 160 // startKey is the level at which to begin clearing the cache, 161 // depending on the configured hierarchy mode.s 162 MergedContextConfiguration startKey = key; 163 if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { 164 MergedContextConfiguration parent = startKey.getParent(); 165 while (parent != null) { 166 startKey = parent; 167 parent = startKey.getParent(); 168 } 169 } 170 171 List<MergedContextConfiguration> removedContexts = new ArrayList<>(); 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 (Map.Entry<MergedContextConfiguration, Set<MergedContextConfiguration>> entry : this.hierarchyMap.entrySet()) { 184 if (entry.getValue().isEmpty()) { 185 this.hierarchyMap.remove(entry.getKey()); 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}