ContextCache.java 9.0 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 54 55 56 57 58
	private final Object monitor = new Object();

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

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

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

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


	/**
74 75
	 * Clears all contexts from the cache and clears context hierarchy information as
	 * well.
A
Arjen Poutsma 已提交
76 77
	 */
	void clear() {
78 79 80 81
		synchronized (monitor) {
			this.contextMap.clear();
			this.hierarchyMap.clear();
		}
A
Arjen Poutsma 已提交
82 83 84
	}

	/**
85
	 * Clears hit and miss count statistics for the cache (i.e., resets counters to zero).
A
Arjen Poutsma 已提交
86 87
	 */
	void clearStatistics() {
88 89
		this.hitCount.set(0);
		this.missCount.set(0);
A
Arjen Poutsma 已提交
90 91 92 93
	}

	/**
	 * Return whether there is a cached context for the given key.
94
	 *
95
	 * @param key the context key (never {@code null})
A
Arjen Poutsma 已提交
96
	 */
97
	boolean contains(MergedContextConfiguration key) {
A
Arjen Poutsma 已提交
98
		Assert.notNull(key, "Key must not be null");
99 100 101
		synchronized (monitor) {
			return this.contextMap.containsKey(key);
		}
A
Arjen Poutsma 已提交
102 103 104
	}

	/**
105 106 107 108 109
	 * Obtain a cached {@code ApplicationContext} for the given key.
	 *
	 * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will be
	 * updated accordingly.
	 *
110
	 * @param key the context key (never {@code null})
111 112
	 * @return the corresponding {@code ApplicationContext} instance, or {@code null} if
	 * not found in the cache.
A
Arjen Poutsma 已提交
113 114
	 * @see #remove
	 */
115
	ApplicationContext get(MergedContextConfiguration key) {
A
Arjen Poutsma 已提交
116
		Assert.notNull(key, "Key must not be null");
117 118 119
		synchronized (monitor) {
			ApplicationContext context = this.contextMap.get(key);
			if (context == null) {
120
				this.missCount.incrementAndGet();
121 122
			}
			else {
123
				this.hitCount.incrementAndGet();
124 125
			}
			return context;
A
Arjen Poutsma 已提交
126 127 128 129
		}
	}

	/**
130 131 132
	 * Get the overall hit count for this cache.
	 * <p>A <em>hit</em> is an access to the cache, which returned a non-null context for
	 * a queried key.
A
Arjen Poutsma 已提交
133 134
	 */
	int getHitCount() {
135
		return this.hitCount.get();
A
Arjen Poutsma 已提交
136 137 138
	}

	/**
139 140 141
	 * 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 已提交
142 143
	 */
	int getMissCount() {
144
		return this.missCount.get();
A
Arjen Poutsma 已提交
145 146 147
	}

	/**
148 149 150
	 * Explicitly add an {@code ApplicationContext} instance to the cache under the given
	 * key.
	 *
151
	 * @param key the context key (never {@code null})
152
	 * @param context the {@code ApplicationContext} instance (never {@code null})
A
Arjen Poutsma 已提交
153
	 */
154
	void put(MergedContextConfiguration key, ApplicationContext context) {
A
Arjen Poutsma 已提交
155 156 157
		Assert.notNull(key, "Key must not be null");
		Assert.notNull(context, "ApplicationContext must not be null");

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
		synchronized (monitor) {
			this.contextMap.put(key, context);

			MergedContextConfiguration child = key;
			MergedContextConfiguration parent = child.getParent();
			while (parent != null) {
				Set<MergedContextConfiguration> list = hierarchyMap.get(parent);
				if (list == null) {
					list = new HashSet<MergedContextConfiguration>();
					hierarchyMap.put(parent, list);
				}
				list.add(child);
				child = parent;
				parent = child.getParent();
			}
		}
A
Arjen Poutsma 已提交
174 175 176
	}

	/**
177 178
	 * Remove the context with the given key from the cache and explicitly
	 * {@linkplain ConfigurableApplicationContext#close() close} it if it is an
179
	 * instance of {@link ConfigurableApplicationContext}.
180
	 *
181 182 183
	 * <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.
184 185 186 187 188 189 190
	 *
	 * <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 已提交
191
	 */
192
	void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) {
A
Arjen Poutsma 已提交
193
		Assert.notNull(key, "Key must not be null");
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247

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

		synchronized (monitor) {
			final List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>();

			remove(removedContexts, startKey);

			// Remove all remaining references to any removed contexts from the
			// hierarchy map.
			for (MergedContextConfiguration currentKey : removedContexts) {
				for (Set<MergedContextConfiguration> children : hierarchyMap.values()) {
					children.remove(currentKey);
				}
			}

			// Remove empty entries from the hierarchy map.
			for (MergedContextConfiguration currentKey : hierarchyMap.keySet()) {
				if (hierarchyMap.get(currentKey).isEmpty()) {
					hierarchyMap.remove(currentKey);
				}
			}
		}
	}

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

		synchronized (monitor) {
			Set<MergedContextConfiguration> children = 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.
				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 = contextMap.remove(key);
			if (context instanceof ConfigurableApplicationContext) {
				((ConfigurableApplicationContext) context).close();
			}
			removedContexts.add(key);
A
Arjen Poutsma 已提交
248 249 250 251
		}
	}

	/**
252 253
	 * 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 已提交
254 255 256
	 * <tt>Integer.MAX_VALUE</tt>.
	 */
	int size() {
257 258 259 260 261 262 263 264 265 266 267 268
		synchronized (monitor) {
			return this.contextMap.size();
		}
	}

	/**
	 * Determine the number of parent contexts currently tracked within the cache.
	 */
	int getParentContextCount() {
		synchronized (monitor) {
			return this.hierarchyMap.size();
		}
A
Arjen Poutsma 已提交
269 270 271
	}

	/**
272 273 274
	 * Generates a text string, which contains the {@linkplain #size() size} as well
	 * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss}, and
	 * {@linkplain #getParentContextCount() parent context} counts.
A
Arjen Poutsma 已提交
275
	 */
276
	@Override
A
Arjen Poutsma 已提交
277
	public String toString() {
278 279 280 281 282 283
		return new ToStringCreator(this)//
		.append("size", size())//
		.append("hitCount", getHitCount())//
		.append("missCount", getMissCount())//
		.append("parentContextCount", getParentContextCount())//
		.toString();
A
Arjen Poutsma 已提交
284 285 286
	}

}