Convert ContextCache to interface with default implementation

- ContextCache is now a public interface.

 - Introduced public DefaultContextCache implementation in the 'support'
   subpackage.

Issue: SPR-12683
This commit is contained in:
Sam Brannen
2015-04-19 00:59:54 +02:00
parent c9d597f519
commit e6c24f7167
5 changed files with 335 additions and 226 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@@ -23,7 +23,7 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
/**
* A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain
* #loadContext loading} and {@linkplain #closeContext closing} application
* contexts, interacting transparently with a <em>context cache</em> behind
* contexts, interacting transparently with a {@link ContextCache} behind
* the scenes.
*
* <p>Note: {@code CacheAwareContextLoaderDelegate} does not extend the
@@ -38,7 +38,7 @@ public interface CacheAwareContextLoaderDelegate {
* Load the {@linkplain ApplicationContext application context} for the supplied
* {@link MergedContextConfiguration} by delegating to the {@link ContextLoader}
* configured in the given {@code MergedContextConfiguration}.
* <p>If the context is present in the <em>context cache</em> it will simply
* <p>If the context is present in the {@code ContextCache} it will simply
* be returned; otherwise, it will be loaded, stored in the cache, and returned.
* @param mergedContextConfiguration the merged context configuration to use
* to load the application context; never {@code null}
@@ -50,7 +50,7 @@ public interface CacheAwareContextLoaderDelegate {
/**
* Remove the {@linkplain ApplicationContext application context} for the
* supplied {@link MergedContextConfiguration} from the <em>context cache</em>
* supplied {@link MergedContextConfiguration} from the {@code ContextCache}
* and {@linkplain ConfigurableApplicationContext#close() close} it if it is
* an instance of {@link ConfigurableApplicationContext}.
* <p>The semantics of the supplied {@code HierarchyMode} must be honored when

View File

@@ -16,145 +16,48 @@
package org.springframework.test.context;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
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.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
* Cache for Spring {@link ApplicationContext ApplicationContexts} in a test
* environment.
* {@code ContextCache} defines the public API for caching Spring
* {@link ApplicationContext ApplicationContexts} within the <em>Spring
* TestContext Framework</em>.
*
* <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts}
* keyed by {@link MergedContextConfiguration} instances.
*
* <h3>Rationale</h3>
* <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.
*
* <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.
* <p>Context caching can have significant performance benefits if context
* initialization is complex. So, although initializing a Spring context itself
* is typically very quick, some beans in a context &mdash; for example, an
* in-memory database or a {@code LocalSessionFactoryBean} for working with
* Hibernate &mdash; may take several seconds to initialize. Hence it often
* makes sense to perform that initialization only once per test suite.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
* @see ConcurrentReferenceHashMap
* @since 4.2
*/
class ContextCache {
/**
* Map of context keys to Spring {@code ApplicationContext} instances.
*/
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
new ConcurrentReferenceHashMap<MergedContextConfiguration, ApplicationContext>(64);
/**
* 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 ConcurrentReferenceHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
private final AtomicInteger hitCount = new AtomicInteger();
private final AtomicInteger missCount = new AtomicInteger();
/**
* Reset all state maintained by this cache.
* @see #clear()
* @see #clearStatistics()
*/
public void reset() {
synchronized (contextMap) {
clear();
clearStatistics();
}
}
/**
* Clear all contexts from the cache and clear context hierarchy information as well.
*/
public void clear() {
synchronized (contextMap) {
this.contextMap.clear();
this.hierarchyMap.clear();
}
}
/**
* Clear hit and miss count statistics for the cache (i.e., reset counters to zero).
*/
public void clearStatistics() {
synchronized (contextMap) {
this.hitCount.set(0);
this.missCount.set(0);
}
}
public interface ContextCache {
/**
* Determine whether there is a cached context for the given key.
* @param key the context key (never {@code null})
* @return {@code true} if the cache contains a context with the given key
*/
public boolean contains(MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
return this.contextMap.containsKey(key);
}
boolean contains(MergedContextConfiguration key);
/**
* Obtain a cached {@code ApplicationContext} for the given key.
* <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will
* be updated accordingly.
* <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts
* must be updated accordingly.
* @param key the context key (never {@code null})
* @return the corresponding {@code ApplicationContext} instance, or {@code null}
* if not found in the cache
* @see #remove
*/
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;
}
/**
* Get the overall hit count for this cache.
* <p>A <em>hit</em> is any access to the cache that returns a non-null
* context for the queried key.
*/
public int getHitCount() {
return this.hitCount.get();
}
/**
* Get the overall miss count for this cache.
* <p>A <em>miss</em> is any access to the cache that returns a {@code null}
* context for the queried key.
*/
public int getMissCount() {
return this.missCount.get();
}
ApplicationContext get(MergedContextConfiguration key);
/**
* Explicitly add an {@code ApplicationContext} instance to the cache
@@ -162,122 +65,80 @@ class ContextCache {
* @param key the context key (never {@code null})
* @param context the {@code ApplicationContext} instance (never {@code null})
*/
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<MergedContextConfiguration>();
this.hierarchyMap.put(parent, list);
}
list.add(child);
child = parent;
parent = child.getParent();
}
}
void put(MergedContextConfiguration key, ApplicationContext context);
/**
* Remove the context with the given key from the cache and explicitly
* {@linkplain ConfigurableApplicationContext#close() close} it if it is an
* instance of {@link ConfigurableApplicationContext}.
* <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.
* <p>In addition, the semantics of the supplied {@code HierarchyMode} will
* {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close}
* it if it is an instance of {@code ConfigurableApplicationContext}.
* <p>Generally speaking, this method should be called if the state of
* a singleton bean has been modified, potentially affecting future
* interaction with the context.
* <p>In addition, the semantics of the supplied {@code HierarchyMode} must
* 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
*/
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<MergedContextConfiguration>();
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);
}
void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode);
/**
* 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}.
* this method must return {@code Integer.MAX_VALUE}.
*/
public int size() {
return this.contextMap.size();
}
int size();
/**
* Determine the number of parent contexts currently tracked within the cache.
*/
public int getParentContextCount() {
return this.hierarchyMap.size();
}
int getParentContextCount();
/**
* 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
* Get the overall hit count for this cache.
* <p>A <em>hit</em> is any access to the cache that returns a non-null
* context for the queried key.
*/
int getHitCount();
/**
* Get the overall miss count for this cache.
* <p>A <em>miss</em> is any access to the cache that returns a {@code null}
* context for the queried key.
*/
int getMissCount();
/**
* Reset all state maintained by this cache including statistics.
* @see #clear()
* @see #clearStatistics()
*/
void reset();
/**
* Clear all contexts from the cache, clearing context hierarchy information as well.
*/
void clear();
/**
* Clear hit and miss count statistics for the cache (i.e., reset counters to zero).
*/
void clearStatistics();
/**
* Generate a text string containing the implementation type of this
* cache and its statistics.
* <p>The value returned by this method will be used primarily for
* logging purposes.
* <p>Specifically, the returned string should contain the name of the
* concrete {@code ContextCache} implementation, the {@linkplain #size},
* {@linkplain #getParentContextCount() parent context count},
* {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount()
* miss count}, and any other information useful in monitoring the
* state of this cache.
* @return a string representation of this cache, including statistics
*/
@Override
public String toString() {
return new ToStringCreator(this)
.append("size", size())
.append("hitCount", getHitCount())
.append("missCount", getMissCount())
.append("parentContextCount", getParentContextCount())
.toString();
}
abstract String toString();
}

View File

@@ -21,16 +21,12 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.support.DefaultContextCache;
import org.springframework.util.Assert;
/**
* Default implementation of the {@link CacheAwareContextLoaderDelegate} interface.
*
* <p>Although {@code DefaultCacheAwareContextLoaderDelegate} was first introduced
* in Spring Framework 4.1, the initial implementation of this class was extracted
* from the existing code base for {@code CacheAwareContextLoaderDelegate} when
* {@code CacheAwareContextLoaderDelegate} was converted into an interface.
*
* @author Sam Brannen
* @since 4.1
*/
@@ -47,21 +43,21 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD
* and reused for all subsequent tests that declare the same unique
* context configuration within the same JVM process.
*/
static final ContextCache defaultContextCache = new ContextCache();
static final ContextCache defaultContextCache = new DefaultContextCache();
private final ContextCache contextCache;
/**
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} that
* uses the default, static {@code ContextCache}.
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
* a static {@code DefaultContextCache}.
*/
DefaultCacheAwareContextLoaderDelegate() {
this(defaultContextCache);
}
/**
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} with
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
* the supplied {@code ContextCache}.
*/
DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) {

View File

@@ -0,0 +1,252 @@
/*
* Copyright 2002-2015 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.support;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
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.ContextCache;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
* Default implementation of the {@link ContextCache} API.
*
* <p>Uses Spring's {@link ConcurrentReferenceHashMap} to store
* {@linkplain java.lang.ref.SoftReference soft references} to cached
* contexts and {@code MergedContextConfiguration} instances.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
* @see ConcurrentReferenceHashMap
*/
public class DefaultContextCache implements ContextCache {
/**
* Map of context keys to Spring {@code ApplicationContext} instances.
*/
private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
new ConcurrentReferenceHashMap<MergedContextConfiguration, ApplicationContext>(64);
/**
* 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 ConcurrentReferenceHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64);
private final AtomicInteger hitCount = new AtomicInteger();
private final AtomicInteger missCount = new AtomicInteger();
/**
* {@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<MergedContextConfiguration>();
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<MergedContextConfiguration>();
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();
}
/**
* {@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 (contextMap) {
clear();
clearStatistics();
}
}
/**
* {@inheritDoc}
*/
@Override
public void clear() {
synchronized (contextMap) {
this.contextMap.clear();
this.hierarchyMap.clear();
}
}
/**
* {@inheritDoc}
*/
@Override
public void clearStatistics() {
synchronized (contextMap) {
this.hitCount.set(0);
this.missCount.set(0);
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return new ToStringCreator(this)
.append("size", size())
.append("parentContextCount", getParentContextCount())
.append("hitCount", getHitCount())
.append("missCount", getMissCount())
.toString();
}
}