/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.test.context.cache; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.util.Assert; /** * Default implementation of the {@link ContextCache} API. * * <p>Uses a synchronized {@link Map} configured with a maximum size * and a <em>least recently used</em> (LRU) eviction policy to cache * {@link ApplicationContext} instances. * * <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int) * constructor argument} or set via a system property or Spring property named * {@code spring.test.context.cache.maxSize}. * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 * @see ContextCacheUtils#retrieveMaxCacheSize() */ public class DefaultContextCache implements ContextCache { private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY); /** * Map of context keys to Spring {@code ApplicationContext} instances. */ private final Map<MergedContextConfiguration, ApplicationContext> contextMap = Collections.synchronizedMap(new LruCache(32, 0.75f)); /** * Map of parent keys to sets of children keys, representing a top-down <em>tree</em> * of context hierarchies. This information is used for determining which subtrees * need to be recursively removed and closed when removing a context that is a parent * of other contexts. */ private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = new ConcurrentHashMap<>(32); private final int maxSize; private final AtomicInteger hitCount = new AtomicInteger(); private final AtomicInteger missCount = new AtomicInteger(); /** * Create a new {@code DefaultContextCache} using the maximum cache size * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}. * @since 4.3 * @see #DefaultContextCache(int) * @see ContextCacheUtils#retrieveMaxCacheSize() */ public DefaultContextCache() { this(ContextCacheUtils.retrieveMaxCacheSize()); } /** * Create a new {@code DefaultContextCache} using the supplied maximum * cache size. * @param maxSize the maximum cache size * @throws IllegalArgumentException if the supplied {@code maxSize} value * is not positive * @since 4.3 * @see #DefaultContextCache() */ public DefaultContextCache(int maxSize) { Assert.isTrue(maxSize > 0, "'maxSize' must be positive"); this.maxSize = maxSize; } /** * {@inheritDoc} */ @Override public boolean contains(MergedContextConfiguration key) { Assert.notNull(key, "Key must not be null"); return this.contextMap.containsKey(key); } /** * {@inheritDoc} */ @Override public ApplicationContext get(MergedContextConfiguration key) { Assert.notNull(key, "Key must not be null"); ApplicationContext context = this.contextMap.get(key); if (context == null) { this.missCount.incrementAndGet(); } else { this.hitCount.incrementAndGet(); } return context; } /** * {@inheritDoc} */ @Override public void put(MergedContextConfiguration key, ApplicationContext context) { Assert.notNull(key, "Key must not be null"); Assert.notNull(context, "ApplicationContext must not be null"); this.contextMap.put(key, context); MergedContextConfiguration child = key; MergedContextConfiguration parent = child.getParent(); while (parent != null) { Set<MergedContextConfiguration> list = this.hierarchyMap.get(parent); if (list == null) { list = new HashSet<>(); this.hierarchyMap.put(parent, list); } list.add(child); child = parent; parent = child.getParent(); } } /** * {@inheritDoc} */ @Override public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { Assert.notNull(key, "Key must not be null"); // startKey is the level at which to begin clearing the cache, depending // on the configured hierarchy mode. MergedContextConfiguration startKey = key; if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { while (startKey.getParent() != null) { startKey = startKey.getParent(); } } List<MergedContextConfiguration> removedContexts = new ArrayList<>(); remove(removedContexts, startKey); // Remove all remaining references to any removed contexts from the // hierarchy map. for (MergedContextConfiguration currentKey : removedContexts) { for (Set<MergedContextConfiguration> children : this.hierarchyMap.values()) { children.remove(currentKey); } } // Remove empty entries from the hierarchy map. for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { if (this.hierarchyMap.get(currentKey).isEmpty()) { this.hierarchyMap.remove(currentKey); } } } private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) { Assert.notNull(key, "Key must not be null"); Set<MergedContextConfiguration> children = this.hierarchyMap.get(key); if (children != null) { for (MergedContextConfiguration child : children) { // Recurse through lower levels remove(removedContexts, child); } // Remove the set of children for the current context from the hierarchy map. this.hierarchyMap.remove(key); } // Physically remove and close leaf nodes first (i.e., on the way back up the // stack as opposed to prior to the recursive call). ApplicationContext context = this.contextMap.remove(key); if (context instanceof ConfigurableApplicationContext) { ((ConfigurableApplicationContext) context).close(); } removedContexts.add(key); } /** * {@inheritDoc} */ @Override public int size() { return this.contextMap.size(); } /** * Get the maximum size of this cache. */ public int getMaxSize() { return this.maxSize; } /** * {@inheritDoc} */ @Override public int getParentContextCount() { return this.hierarchyMap.size(); } /** * {@inheritDoc} */ @Override public int getHitCount() { return this.hitCount.get(); } /** * {@inheritDoc} */ @Override public int getMissCount() { return this.missCount.get(); } /** * {@inheritDoc} */ @Override public void reset() { synchronized (this.contextMap) { clear(); clearStatistics(); } } /** * {@inheritDoc} */ @Override public void clear() { synchronized (this.contextMap) { this.contextMap.clear(); this.hierarchyMap.clear(); } } /** * {@inheritDoc} */ @Override public void clearStatistics() { synchronized (this.contextMap) { this.hitCount.set(0); this.missCount.set(0); } } /** * {@inheritDoc} */ @Override public void logStatistics() { if (statsLogger.isDebugEnabled()) { statsLogger.debug("Spring test ApplicationContext cache statistics: " + this); } } /** * Generate a text string containing the implementation type of this * cache and its statistics. * <p>The string returned by this method contains all information * required for compliance with the contract for {@link #logStatistics()}. * @return a string representation of this cache, including statistics */ @Override public String toString() { return new ToStringCreator(this) .append("size", size()) .append("maxSize", getMaxSize()) .append("parentContextCount", getParentContextCount()) .append("hitCount", getHitCount()) .append("missCount", getMissCount()) .toString(); } /** * Simple cache implementation based on {@link LinkedHashMap} with a maximum * size and a <em>least recently used</em> (LRU) eviction policy that * properly closes application contexts. * @since 4.3 */ @SuppressWarnings("serial") private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> { /** * Create a new {@code LruCache} with the supplied initial capacity * and load factor. * @param initialCapacity the initial capacity * @param loadFactor the load factor */ LruCache(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } @Override protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) { if (this.size() > DefaultContextCache.this.getMaxSize()) { // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally // invoke java.util.Map.remove(Object, Object). DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL); } // Return false since we invoke a custom eviction algorithm. return false; } } }