ContextCache.java 8.7 KB
Newer Older
A
Arjen Poutsma 已提交
1
/*
2
 * Copyright 2002-2014 the original author or authors.
A
Arjen Poutsma 已提交
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 * 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;

19 20 21
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
A
Arjen Poutsma 已提交
22
import java.util.Map;
23
import java.util.Set;
A
Arjen Poutsma 已提交
24
import java.util.concurrent.ConcurrentHashMap;
25
import java.util.concurrent.atomic.AtomicInteger;
A
Arjen Poutsma 已提交
26 27 28 29

import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.style.ToStringCreator;
30
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
A
Arjen Poutsma 已提交
31 32 33
import org.springframework.util.Assert;

/**
34
 * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test environment.
A
Arjen Poutsma 已提交
35
 *
36 37 38 39 40 41 42 43
 * <p>Maintains a cache of {@code ApplicationContexts} keyed by
 * {@link MergedContextConfiguration} instances.
 *
 * <p>This has significant performance benefits if initializing the context would take time.
 * While initializing a Spring context itself is very quick, some beans in a context, such
 * as a {@code LocalSessionFactoryBean} for working with Hibernate, may take some time to
 * initialize. Hence it often makes sense to perform that initialization only once per
 * test suite.
A
Arjen Poutsma 已提交
44 45 46 47 48 49 50
 *
 * @author Sam Brannen
 * @author Juergen Hoeller
 * @since 2.5
 */
class ContextCache {

51 52 53
	/**
	 * Map of context keys to Spring {@code ApplicationContext} instances.
	 */
54 55
	private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
			new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>(64);
56

A
Arjen Poutsma 已提交
57
	/**
58 59 60 61
	 * 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.
A
Arjen Poutsma 已提交
62
	 */
63 64
	private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
			new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
A
Arjen Poutsma 已提交
65

66
	private final AtomicInteger hitCount = new AtomicInteger();
A
Arjen Poutsma 已提交
67

68
	private final AtomicInteger missCount = new AtomicInteger();
A
Arjen Poutsma 已提交
69 70 71


	/**
72
	 * Clear all contexts from the cache and clears context hierarchy information as well.
A
Arjen Poutsma 已提交
73
	 */
74 75 76
	public void clear() {
		this.contextMap.clear();
		this.hierarchyMap.clear();
A
Arjen Poutsma 已提交
77 78 79
	}

	/**
80
	 * Clear hit and miss count statistics for the cache (i.e., resets counters to zero).
A
Arjen Poutsma 已提交
81
	 */
82
	public void clearStatistics() {
83 84
		this.hitCount.set(0);
		this.missCount.set(0);
A
Arjen Poutsma 已提交
85 86 87 88
	}

	/**
	 * Return whether there is a cached context for the given key.
89
	 * @param key the context key (never {@code null})
A
Arjen Poutsma 已提交
90
	 */
91
	public boolean contains(MergedContextConfiguration key) {
A
Arjen Poutsma 已提交
92
		Assert.notNull(key, "Key must not be null");
93
		return this.contextMap.containsKey(key);
A
Arjen Poutsma 已提交
94 95 96
	}

	/**
97
	 * Obtain a cached {@code ApplicationContext} for the given key.
98 99
	 * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will
	 * be updated accordingly.
100
	 * @param key the context key (never {@code null})
101 102
	 * @return the corresponding {@code ApplicationContext} instance, or {@code null}
	 * if not found in the cache
A
Arjen Poutsma 已提交
103 104
	 * @see #remove
	 */
105
	public ApplicationContext get(MergedContextConfiguration key) {
A
Arjen Poutsma 已提交
106
		Assert.notNull(key, "Key must not be null");
107 108 109 110 111 112
		ApplicationContext context = this.contextMap.get(key);
		if (context == null) {
			this.missCount.incrementAndGet();
		}
		else {
			this.hitCount.incrementAndGet();
A
Arjen Poutsma 已提交
113
		}
114
		return context;
A
Arjen Poutsma 已提交
115 116 117
	}

	/**
118
	 * Get the overall hit count for this cache.
119 120
	 * <p>A <em>hit</em> is an access to the cache, which returned a non-null context
	 * for a queried key.
A
Arjen Poutsma 已提交
121
	 */
122
	public int getHitCount() {
123
		return this.hitCount.get();
A
Arjen Poutsma 已提交
124 125 126
	}

	/**
127 128 129
	 * Get the overall miss count for this cache.
	 * <p>A <em>miss</em> is an access to the cache, which returned a {@code null} context
	 * for a queried key.
A
Arjen Poutsma 已提交
130
	 */
131
	public int getMissCount() {
132
		return this.missCount.get();
A
Arjen Poutsma 已提交
133 134 135
	}

	/**
136
	 * Explicitly add an {@code ApplicationContext} instance to the cache under the given key.
137
	 * @param key the context key (never {@code null})
138
	 * @param context the {@code ApplicationContext} instance (never {@code null})
A
Arjen Poutsma 已提交
139
	 */
140
	public void put(MergedContextConfiguration key, ApplicationContext context) {
A
Arjen Poutsma 已提交
141 142 143
		Assert.notNull(key, "Key must not be null");
		Assert.notNull(context, "ApplicationContext must not be null");

144 145 146 147 148 149 150 151
		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<MergedContextConfiguration>();
				this.hierarchyMap.put(parent, list);
152
			}
153 154 155
			list.add(child);
			child = parent;
			parent = child.getParent();
156
		}
A
Arjen Poutsma 已提交
157 158 159
	}

	/**
160 161
	 * Remove the context with the given key from the cache and explicitly
	 * {@linkplain ConfigurableApplicationContext#close() close} it if it is an
162
	 * instance of {@link ConfigurableApplicationContext}.
163 164 165
	 * <p>Generally speaking, you would only call this method if you change the
	 * state of a singleton bean, potentially affecting future interaction with
	 * the context.
166 167 168 169 170
	 * <p>In addition, the semantics of the supplied {@code HierarchyMode} will
	 * be honored. See the Javadoc for {@link HierarchyMode} for details.
	 * @param key the context key; never {@code null}
	 * @param hierarchyMode the hierarchy mode; may be {@code null} if the context
	 * is not part of a hierarchy
A
Arjen Poutsma 已提交
171
	 */
172
	public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) {
A
Arjen Poutsma 已提交
173
		Assert.notNull(key, "Key must not be null");
174 175 176 177 178 179 180 181 182 183

		// 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();
			}
		}

184 185
		List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>();
		remove(removedContexts, startKey);
186

187 188 189 190 191
		// 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);
192
			}
193
		}
194

195 196 197 198
		// Remove empty entries from the hierarchy map.
		for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) {
			if (this.hierarchyMap.get(currentKey).isEmpty()) {
				this.hierarchyMap.remove(currentKey);
199 200 201 202 203 204 205
			}
		}
	}

	private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) {
		Assert.notNull(key, "Key must not be null");

206 207 208 209 210
		Set<MergedContextConfiguration> children = this.hierarchyMap.get(key);
		if (children != null) {
			for (MergedContextConfiguration child : children) {
				// Recurse through lower levels
				remove(removedContexts, child);
211
			}
212 213 214
			// Remove the set of children for the current context from the hierarchy map.
			this.hierarchyMap.remove(key);
		}
215

216 217 218 219 220
		// 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();
A
Arjen Poutsma 已提交
221
		}
222
		removedContexts.add(key);
A
Arjen Poutsma 已提交
223 224 225
	}

	/**
226 227
	 * Determine the number of contexts currently stored in the cache. If the cache
	 * contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
A
Arjen Poutsma 已提交
228 229
	 * <tt>Integer.MAX_VALUE</tt>.
	 */
230 231
	public int size() {
		return this.contextMap.size();
232 233 234 235 236
	}

	/**
	 * Determine the number of parent contexts currently tracked within the cache.
	 */
237 238
	public int getParentContextCount() {
		return this.hierarchyMap.size();
A
Arjen Poutsma 已提交
239 240 241
	}

	/**
242
	 * Generates a text string, which contains the {@linkplain #size() size} as well
243 244
	 * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss},
	 * and {@linkplain #getParentContextCount() parent context} counts.
A
Arjen Poutsma 已提交
245
	 */
246
	@Override
A
Arjen Poutsma 已提交
247
	public String toString() {
248 249 250 251 252 253
		return new ToStringCreator(this)
				.append("size", size())
				.append("hitCount", getHitCount())
				.append("missCount", getMissCount())
				.append("parentContextCount", getParentContextCount())
				.toString();
A
Arjen Poutsma 已提交
254 255 256
	}

}