|
16 | 16 |
|
17 | 17 | package org.springframework.test.context;
|
18 | 18 |
|
19 |
| -import java.util.ArrayList; |
20 |
| -import java.util.HashSet; |
21 |
| -import java.util.List; |
22 |
| -import java.util.Map; |
23 |
| -import java.util.Set; |
24 |
| -import java.util.concurrent.atomic.AtomicInteger; |
25 |
| - |
26 | 19 | import org.springframework.context.ApplicationContext;
|
27 |
| -import org.springframework.context.ConfigurableApplicationContext; |
28 |
| -import org.springframework.core.style.ToStringCreator; |
29 | 20 | import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
|
30 |
| -import org.springframework.util.Assert; |
31 |
| -import org.springframework.util.ConcurrentReferenceHashMap; |
32 | 21 |
|
33 | 22 | /**
|
34 |
| - * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test |
35 |
| - * environment. |
| 23 | + * {@code ContextCache} defines the public API for caching Spring |
| 24 | + * {@link ApplicationContext ApplicationContexts} within the <em>Spring |
| 25 | + * TestContext Framework</em>. |
36 | 26 | *
|
37 |
| - * <h3>Rationale</h3> |
38 |
| - * <p>Caching has significant performance benefits if initializing the context |
39 |
| - * takes a considerable about of time. Although initializing a Spring context |
40 |
| - * itself is very quick, some beans in a context, such as a |
41 |
| - * {@code LocalSessionFactoryBean} for working with Hibernate, may take some |
42 |
| - * time to initialize. Hence it often makes sense to perform that initialization |
43 |
| - * only once per test suite. |
| 27 | + * <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts} |
| 28 | + * keyed by {@link MergedContextConfiguration} instances. |
44 | 29 | *
|
45 |
| - * <h3>Implementation Details</h3> |
46 |
| - * <p>{@code ContextCache} maintains a cache of {@code ApplicationContexts} |
47 |
| - * keyed by {@link MergedContextConfiguration} instances. Behind the scenes, |
48 |
| - * Spring's {@link ConcurrentReferenceHashMap} is used to store |
49 |
| - * {@linkplain java.lang.ref.SoftReference soft references} to cached contexts |
50 |
| - * and {@code MergedContextConfiguration} instances. |
| 30 | + * <h3>Rationale</h3> |
| 31 | + * <p>Context caching can have significant performance benefits if context |
| 32 | + * initialization is complex. So, although initializing a Spring context itself |
| 33 | + * is typically very quick, some beans in a context — for example, an |
| 34 | + * in-memory database or a {@code LocalSessionFactoryBean} for working with |
| 35 | + * Hibernate — may take several seconds to initialize. Hence it often |
| 36 | + * makes sense to perform that initialization only once per test suite. |
51 | 37 | *
|
52 | 38 | * @author Sam Brannen
|
53 | 39 | * @author Juergen Hoeller
|
54 |
| - * @since 2.5 |
55 |
| - * @see ConcurrentReferenceHashMap |
| 40 | + * @since 4.2 |
56 | 41 | */
|
57 |
| -class ContextCache { |
58 |
| - |
59 |
| - /** |
60 |
| - * Map of context keys to Spring {@code ApplicationContext} instances. |
61 |
| - */ |
62 |
| - private final Map<MergedContextConfiguration, ApplicationContext> contextMap = |
63 |
| - new ConcurrentReferenceHashMap<MergedContextConfiguration, ApplicationContext>(64); |
64 |
| - |
65 |
| - /** |
66 |
| - * Map of parent keys to sets of children keys, representing a top-down <em>tree</em> |
67 |
| - * of context hierarchies. This information is used for determining which subtrees |
68 |
| - * need to be recursively removed and closed when removing a context that is a parent |
69 |
| - * of other contexts. |
70 |
| - */ |
71 |
| - private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = |
72 |
| - new ConcurrentReferenceHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64); |
73 |
| - |
74 |
| - private final AtomicInteger hitCount = new AtomicInteger(); |
75 |
| - |
76 |
| - private final AtomicInteger missCount = new AtomicInteger(); |
77 |
| - |
78 |
| - /** |
79 |
| - * Reset all state maintained by this cache. |
80 |
| - * @see #clear() |
81 |
| - * @see #clearStatistics() |
82 |
| - */ |
83 |
| - public void reset() { |
84 |
| - synchronized (contextMap) { |
85 |
| - clear(); |
86 |
| - clearStatistics(); |
87 |
| - } |
88 |
| - } |
89 |
| - |
90 |
| - /** |
91 |
| - * Clear all contexts from the cache and clear context hierarchy information as well. |
92 |
| - */ |
93 |
| - public void clear() { |
94 |
| - synchronized (contextMap) { |
95 |
| - this.contextMap.clear(); |
96 |
| - this.hierarchyMap.clear(); |
97 |
| - } |
98 |
| - } |
99 |
| - |
100 |
| - /** |
101 |
| - * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). |
102 |
| - */ |
103 |
| - public void clearStatistics() { |
104 |
| - synchronized (contextMap) { |
105 |
| - this.hitCount.set(0); |
106 |
| - this.missCount.set(0); |
107 |
| - } |
108 |
| - } |
| 42 | +public interface ContextCache { |
109 | 43 |
|
110 | 44 | /**
|
111 | 45 | * Determine whether there is a cached context for the given key.
|
112 | 46 | * @param key the context key (never {@code null})
|
113 | 47 | * @return {@code true} if the cache contains a context with the given key
|
114 | 48 | */
|
115 |
| - public boolean contains(MergedContextConfiguration key) { |
116 |
| - Assert.notNull(key, "Key must not be null"); |
117 |
| - return this.contextMap.containsKey(key); |
118 |
| - } |
| 49 | + boolean contains(MergedContextConfiguration key); |
119 | 50 |
|
120 | 51 | /**
|
121 | 52 | * Obtain a cached {@code ApplicationContext} for the given key.
|
122 |
| - * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will |
123 |
| - * be updated accordingly. |
| 53 | + * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts |
| 54 | + * must be updated accordingly. |
124 | 55 | * @param key the context key (never {@code null})
|
125 | 56 | * @return the corresponding {@code ApplicationContext} instance, or {@code null}
|
126 | 57 | * if not found in the cache
|
127 | 58 | * @see #remove
|
128 | 59 | */
|
129 |
| - public ApplicationContext get(MergedContextConfiguration key) { |
130 |
| - Assert.notNull(key, "Key must not be null"); |
131 |
| - ApplicationContext context = this.contextMap.get(key); |
132 |
| - if (context == null) { |
133 |
| - this.missCount.incrementAndGet(); |
134 |
| - } |
135 |
| - else { |
136 |
| - this.hitCount.incrementAndGet(); |
137 |
| - } |
138 |
| - return context; |
139 |
| - } |
140 |
| - |
141 |
| - /** |
142 |
| - * Get the overall hit count for this cache. |
143 |
| - * <p>A <em>hit</em> is any access to the cache that returns a non-null |
144 |
| - * context for the queried key. |
145 |
| - */ |
146 |
| - public int getHitCount() { |
147 |
| - return this.hitCount.get(); |
148 |
| - } |
149 |
| - |
150 |
| - /** |
151 |
| - * Get the overall miss count for this cache. |
152 |
| - * <p>A <em>miss</em> is any access to the cache that returns a {@code null} |
153 |
| - * context for the queried key. |
154 |
| - */ |
155 |
| - public int getMissCount() { |
156 |
| - return this.missCount.get(); |
157 |
| - } |
| 60 | + ApplicationContext get(MergedContextConfiguration key); |
158 | 61 |
|
159 | 62 | /**
|
160 | 63 | * Explicitly add an {@code ApplicationContext} instance to the cache
|
161 | 64 | * under the given key.
|
162 | 65 | * @param key the context key (never {@code null})
|
163 | 66 | * @param context the {@code ApplicationContext} instance (never {@code null})
|
164 | 67 | */
|
165 |
| - public void put(MergedContextConfiguration key, ApplicationContext context) { |
166 |
| - Assert.notNull(key, "Key must not be null"); |
167 |
| - Assert.notNull(context, "ApplicationContext must not be null"); |
168 |
| - |
169 |
| - this.contextMap.put(key, context); |
170 |
| - MergedContextConfiguration child = key; |
171 |
| - MergedContextConfiguration parent = child.getParent(); |
172 |
| - while (parent != null) { |
173 |
| - Set<MergedContextConfiguration> list = this.hierarchyMap.get(parent); |
174 |
| - if (list == null) { |
175 |
| - list = new HashSet<MergedContextConfiguration>(); |
176 |
| - this.hierarchyMap.put(parent, list); |
177 |
| - } |
178 |
| - list.add(child); |
179 |
| - child = parent; |
180 |
| - parent = child.getParent(); |
181 |
| - } |
182 |
| - } |
| 68 | + void put(MergedContextConfiguration key, ApplicationContext context); |
183 | 69 |
|
184 | 70 | /**
|
185 | 71 | * Remove the context with the given key from the cache and explicitly
|
186 |
| - * {@linkplain ConfigurableApplicationContext#close() close} it if it is an |
187 |
| - * instance of {@link ConfigurableApplicationContext}. |
188 |
| - * <p>Generally speaking, you would only call this method if you change the |
189 |
| - * state of a singleton bean, potentially affecting future interaction with |
190 |
| - * the context. |
191 |
| - * <p>In addition, the semantics of the supplied {@code HierarchyMode} will |
| 72 | + * {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close} |
| 73 | + * it if it is an instance of {@code ConfigurableApplicationContext}. |
| 74 | + * <p>Generally speaking, this method should be called if the state of |
| 75 | + * a singleton bean has been modified, potentially affecting future |
| 76 | + * interaction with the context. |
| 77 | + * <p>In addition, the semantics of the supplied {@code HierarchyMode} must |
192 | 78 | * be honored. See the Javadoc for {@link HierarchyMode} for details.
|
193 | 79 | * @param key the context key; never {@code null}
|
194 | 80 | * @param hierarchyMode the hierarchy mode; may be {@code null} if the context
|
195 | 81 | * is not part of a hierarchy
|
196 | 82 | */
|
197 |
| - public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { |
198 |
| - Assert.notNull(key, "Key must not be null"); |
| 83 | + void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode); |
199 | 84 |
|
200 |
| - // startKey is the level at which to begin clearing the cache, depending |
201 |
| - // on the configured hierarchy mode. |
202 |
| - MergedContextConfiguration startKey = key; |
203 |
| - if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { |
204 |
| - while (startKey.getParent() != null) { |
205 |
| - startKey = startKey.getParent(); |
206 |
| - } |
207 |
| - } |
208 |
| - |
209 |
| - List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>(); |
210 |
| - remove(removedContexts, startKey); |
211 |
| - |
212 |
| - // Remove all remaining references to any removed contexts from the |
213 |
| - // hierarchy map. |
214 |
| - for (MergedContextConfiguration currentKey : removedContexts) { |
215 |
| - for (Set<MergedContextConfiguration> children : this.hierarchyMap.values()) { |
216 |
| - children.remove(currentKey); |
217 |
| - } |
218 |
| - } |
| 85 | + /** |
| 86 | + * Determine the number of contexts currently stored in the cache. |
| 87 | + * <p>If the cache contains more than {@code Integer.MAX_VALUE} elements, |
| 88 | + * this method must return {@code Integer.MAX_VALUE}. |
| 89 | + */ |
| 90 | + int size(); |
219 | 91 |
|
220 |
| - // Remove empty entries from the hierarchy map. |
221 |
| - for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { |
222 |
| - if (this.hierarchyMap.get(currentKey).isEmpty()) { |
223 |
| - this.hierarchyMap.remove(currentKey); |
224 |
| - } |
225 |
| - } |
226 |
| - } |
| 92 | + /** |
| 93 | + * Determine the number of parent contexts currently tracked within the cache. |
| 94 | + */ |
| 95 | + int getParentContextCount(); |
227 | 96 |
|
228 |
| - private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) { |
229 |
| - Assert.notNull(key, "Key must not be null"); |
| 97 | + /** |
| 98 | + * Get the overall hit count for this cache. |
| 99 | + * <p>A <em>hit</em> is any access to the cache that returns a non-null |
| 100 | + * context for the queried key. |
| 101 | + */ |
| 102 | + int getHitCount(); |
230 | 103 |
|
231 |
| - Set<MergedContextConfiguration> children = this.hierarchyMap.get(key); |
232 |
| - if (children != null) { |
233 |
| - for (MergedContextConfiguration child : children) { |
234 |
| - // Recurse through lower levels |
235 |
| - remove(removedContexts, child); |
236 |
| - } |
237 |
| - // Remove the set of children for the current context from the hierarchy map. |
238 |
| - this.hierarchyMap.remove(key); |
239 |
| - } |
| 104 | + /** |
| 105 | + * Get the overall miss count for this cache. |
| 106 | + * <p>A <em>miss</em> is any access to the cache that returns a {@code null} |
| 107 | + * context for the queried key. |
| 108 | + */ |
| 109 | + int getMissCount(); |
240 | 110 |
|
241 |
| - // Physically remove and close leaf nodes first (i.e., on the way back up the |
242 |
| - // stack as opposed to prior to the recursive call). |
243 |
| - ApplicationContext context = this.contextMap.remove(key); |
244 |
| - if (context instanceof ConfigurableApplicationContext) { |
245 |
| - ((ConfigurableApplicationContext) context).close(); |
246 |
| - } |
247 |
| - removedContexts.add(key); |
248 |
| - } |
| 111 | + /** |
| 112 | + * Reset all state maintained by this cache including statistics. |
| 113 | + * @see #clear() |
| 114 | + * @see #clearStatistics() |
| 115 | + */ |
| 116 | + void reset(); |
249 | 117 |
|
250 | 118 | /**
|
251 |
| - * Determine the number of contexts currently stored in the cache. |
252 |
| - * <p>If the cache contains more than {@code Integer.MAX_VALUE} elements, |
253 |
| - * this method returns {@code Integer.MAX_VALUE}. |
| 119 | + * Clear all contexts from the cache, clearing context hierarchy information as well. |
254 | 120 | */
|
255 |
| - public int size() { |
256 |
| - return this.contextMap.size(); |
257 |
| - } |
| 121 | + void clear(); |
258 | 122 |
|
259 | 123 | /**
|
260 |
| - * Determine the number of parent contexts currently tracked within the cache. |
| 124 | + * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). |
261 | 125 | */
|
262 |
| - public int getParentContextCount() { |
263 |
| - return this.hierarchyMap.size(); |
264 |
| - } |
| 126 | + void clearStatistics(); |
265 | 127 |
|
266 | 128 | /**
|
267 |
| - * Generate a text string containing the statistics for this cache. |
268 |
| - * <p>Specifically, the returned string contains the {@linkplain #size}, |
269 |
| - * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() miss count}, |
270 |
| - * and {@linkplain #getParentContextCount() parent context count}. |
271 |
| - * @return the statistics for this cache |
| 129 | + * Generate a text string containing the implementation type of this |
| 130 | + * cache and its statistics. |
| 131 | + * <p>The value returned by this method will be used primarily for |
| 132 | + * logging purposes. |
| 133 | + * <p>Specifically, the returned string should contain the name of the |
| 134 | + * concrete {@code ContextCache} implementation, the {@linkplain #size}, |
| 135 | + * {@linkplain #getParentContextCount() parent context count}, |
| 136 | + * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() |
| 137 | + * miss count}, and any other information useful in monitoring the |
| 138 | + * state of this cache. |
| 139 | + * @return a string representation of this cache, including statistics |
272 | 140 | */
|
273 | 141 | @Override
|
274 |
| - public String toString() { |
275 |
| - return new ToStringCreator(this) |
276 |
| - .append("size", size()) |
277 |
| - .append("hitCount", getHitCount()) |
278 |
| - .append("missCount", getMissCount()) |
279 |
| - .append("parentContextCount", getParentContextCount()) |
280 |
| - .toString(); |
281 |
| - } |
| 142 | + abstract String toString(); |
282 | 143 |
|
283 | 144 | }
|
0 commit comments