ContextCache.java 9.5 KB
Newer Older
A
Arjen Poutsma 已提交
1
/*
2
 * Copyright 2002-2015 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;
24
import java.util.concurrent.atomic.AtomicInteger;
A
Arjen Poutsma 已提交
25 26 27 28

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

/**
34 35
 * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test
 * environment.
A
Arjen Poutsma 已提交
36
 *
37
 * <h3>Rationale</h3>
38 39 40 41 42 43
 * <p>Caching has significant performance benefits if initializing the context
 * takes a considerable about of time. Although 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 51
 * <h3>Implementation Details</h3>
 * <p>{@code ContextCache} maintains a cache of {@code ApplicationContexts}
 * keyed by {@link MergedContextConfiguration} instances. Behind the scenes,
 * Spring's {@link ConcurrentReferenceHashMap} is used to store
 * {@linkplain java.lang.ref.SoftReference soft references} to cached contexts
 * and {@code MergedContextConfiguration} instances.
 *
A
Arjen Poutsma 已提交
52 53 54
 * @author Sam Brannen
 * @author Juergen Hoeller
 * @since 2.5
55
 * @see ConcurrentReferenceHashMap
A
Arjen Poutsma 已提交
56 57 58
 */
class ContextCache {

59 60 61
	/**
	 * Map of context keys to Spring {@code ApplicationContext} instances.
	 */
62
	private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
63
			new ConcurrentReferenceHashMap<MergedContextConfiguration, ApplicationContext>(64);
64

A
Arjen Poutsma 已提交
65
	/**
66 67 68 69
	 * 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 已提交
70
	 */
71
	private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
72
			new ConcurrentReferenceHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
A
Arjen Poutsma 已提交
73

74
	private final AtomicInteger hitCount = new AtomicInteger();
A
Arjen Poutsma 已提交
75

76
	private final AtomicInteger missCount = new AtomicInteger();
A
Arjen Poutsma 已提交
77

78 79 80 81 82 83 84 85 86 87 88
	/**
	 * Reset all state maintained by this cache.
	 * @see #clear()
	 * @see #clearStatistics()
	 */
	public void reset() {
		synchronized (contextMap) {
			clear();
			clearStatistics();
		}
	}
A
Arjen Poutsma 已提交
89 90

	/**
S
Sam Brannen 已提交
91
	 * Clear all contexts from the cache and clear context hierarchy information as well.
A
Arjen Poutsma 已提交
92
	 */
93
	public void clear() {
94 95 96 97
		synchronized (contextMap) {
			this.contextMap.clear();
			this.hierarchyMap.clear();
		}
A
Arjen Poutsma 已提交
98 99 100
	}

	/**
S
Sam Brannen 已提交
101
	 * Clear hit and miss count statistics for the cache (i.e., reset counters to zero).
A
Arjen Poutsma 已提交
102
	 */
103
	public void clearStatistics() {
104 105 106 107
		synchronized (contextMap) {
			this.hitCount.set(0);
			this.missCount.set(0);
		}
A
Arjen Poutsma 已提交
108 109 110
	}

	/**
S
Sam Brannen 已提交
111
	 * Determine whether there is a cached context for the given key.
112
	 * @param key the context key (never {@code null})
S
Sam Brannen 已提交
113
	 * @return {@code true} if the cache contains a context with the given key
A
Arjen Poutsma 已提交
114
	 */
115
	public boolean contains(MergedContextConfiguration key) {
A
Arjen Poutsma 已提交
116
		Assert.notNull(key, "Key must not be null");
117
		return this.contextMap.containsKey(key);
A
Arjen Poutsma 已提交
118 119 120
	}

	/**
121
	 * Obtain a cached {@code ApplicationContext} for the given key.
122 123
	 * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will
	 * be updated accordingly.
124
	 * @param key the context key (never {@code null})
125 126
	 * @return the corresponding {@code ApplicationContext} instance, or {@code null}
	 * if not found in the cache
A
Arjen Poutsma 已提交
127 128
	 * @see #remove
	 */
129
	public ApplicationContext get(MergedContextConfiguration key) {
A
Arjen Poutsma 已提交
130
		Assert.notNull(key, "Key must not be null");
131 132 133 134 135 136
		ApplicationContext context = this.contextMap.get(key);
		if (context == null) {
			this.missCount.incrementAndGet();
		}
		else {
			this.hitCount.incrementAndGet();
A
Arjen Poutsma 已提交
137
		}
138
		return context;
A
Arjen Poutsma 已提交
139 140 141
	}

	/**
142
	 * Get the overall hit count for this cache.
143 144
	 * <p>A <em>hit</em> is any access to the cache that returns a non-null
	 * context for the queried key.
A
Arjen Poutsma 已提交
145
	 */
146
	public int getHitCount() {
147
		return this.hitCount.get();
A
Arjen Poutsma 已提交
148 149 150
	}

	/**
151
	 * Get the overall miss count for this cache.
152 153
	 * <p>A <em>miss</em> is any access to the cache that returns a {@code null}
	 * context for the queried key.
A
Arjen Poutsma 已提交
154
	 */
155
	public int getMissCount() {
156
		return this.missCount.get();
A
Arjen Poutsma 已提交
157 158 159
	}

	/**
160 161
	 * Explicitly add an {@code ApplicationContext} instance to the cache
	 * under the given key.
162
	 * @param key the context key (never {@code null})
163
	 * @param context the {@code ApplicationContext} instance (never {@code null})
A
Arjen Poutsma 已提交
164
	 */
165
	public void put(MergedContextConfiguration key, ApplicationContext context) {
A
Arjen Poutsma 已提交
166 167 168
		Assert.notNull(key, "Key must not be null");
		Assert.notNull(context, "ApplicationContext must not be null");

169 170 171 172 173 174 175 176
		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);
177
			}
178 179 180
			list.add(child);
			child = parent;
			parent = child.getParent();
181
		}
A
Arjen Poutsma 已提交
182 183 184
	}

	/**
185 186
	 * Remove the context with the given key from the cache and explicitly
	 * {@linkplain ConfigurableApplicationContext#close() close} it if it is an
187
	 * instance of {@link ConfigurableApplicationContext}.
188 189 190
	 * <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.
191 192 193 194 195
	 * <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 已提交
196
	 */
197
	public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) {
A
Arjen Poutsma 已提交
198
		Assert.notNull(key, "Key must not be null");
199 200 201 202 203 204 205 206 207 208

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

209 210
		List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>();
		remove(removedContexts, startKey);
211

212 213 214 215 216
		// 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);
217
			}
218
		}
219

220 221 222 223
		// Remove empty entries from the hierarchy map.
		for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) {
			if (this.hierarchyMap.get(currentKey).isEmpty()) {
				this.hierarchyMap.remove(currentKey);
224 225 226 227 228 229 230
			}
		}
	}

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

231 232 233 234 235
		Set<MergedContextConfiguration> children = this.hierarchyMap.get(key);
		if (children != null) {
			for (MergedContextConfiguration child : children) {
				// Recurse through lower levels
				remove(removedContexts, child);
236
			}
237 238 239
			// Remove the set of children for the current context from the hierarchy map.
			this.hierarchyMap.remove(key);
		}
240

241 242 243 244 245
		// 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 已提交
246
		}
247
		removedContexts.add(key);
A
Arjen Poutsma 已提交
248 249 250
	}

	/**
S
Sam Brannen 已提交
251 252 253
	 * Determine the number of contexts currently stored in the cache.
	 * <p>If the cache contains more than {@code Integer.MAX_VALUE} elements,
	 * this method returns {@code Integer.MAX_VALUE}.
A
Arjen Poutsma 已提交
254
	 */
255 256
	public int size() {
		return this.contextMap.size();
257 258 259 260 261
	}

	/**
	 * Determine the number of parent contexts currently tracked within the cache.
	 */
262 263
	public int getParentContextCount() {
		return this.hierarchyMap.size();
A
Arjen Poutsma 已提交
264 265 266
	}

	/**
267 268 269 270 271
	 * Generate a text string containing the statistics for this cache.
	 * <p>Specifically, the returned string contains the {@linkplain #size},
	 * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() miss count},
	 * and {@linkplain #getParentContextCount() parent context count}.
	 * @return the statistics for this cache
A
Arjen Poutsma 已提交
272
	 */
273
	@Override
A
Arjen Poutsma 已提交
274
	public String toString() {
275 276 277 278 279 280
		return new ToStringCreator(this)
				.append("size", size())
				.append("hitCount", getHitCount())
				.append("missCount", getMissCount())
				.append("parentContextCount", getParentContextCount())
				.toString();
A
Arjen Poutsma 已提交
281 282 283
	}

}