Merge pull request #780 from sbrannen/SPR-12683

Improve extensibility of TestContext bootstrapping & context caching

These commits include numerous refactorings and enhancements to the
bootstrapping and context caching mechanisms in the Spring TestContext
Framework.

Issue: SPR-12683
This commit is contained in:
Sam Brannen 2015-04-19 19:44:15 +02:00
commit 9e6a5ae4a7
18 changed files with 674 additions and 339 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,14 +28,15 @@ package org.springframework.test.context;
public interface BootstrapContext { public interface BootstrapContext {
/** /**
* Get the {@link Class test class} for this bootstrap context. * Get the {@linkplain Class test class} for this bootstrap context.
* @return the test class (never {@code null}) * @return the test class (never {@code null})
*/ */
Class<?> getTestClass(); Class<?> getTestClass();
/** /**
* Get the {@link CacheAwareContextLoaderDelegate} to use for transparent * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent
* interaction with the <em>context cache</em>. * interaction with the {@code ContextCache}.
* @return the context loader delegate (never {@code null})
*/ */
CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate(); CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate();

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@
package org.springframework.test.context; package org.springframework.test.context;
import java.lang.reflect.Constructor;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -36,6 +38,10 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
*/ */
abstract class BootstrapUtils { abstract class BootstrapUtils {
private static final String DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME = "org.springframework.test.context.support.DefaultBootstrapContext";
private static final String DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME = "org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate";
private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper"; private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper";
private static final Log logger = LogFactory.getLog(BootstrapUtils.class); private static final Log logger = LogFactory.getLog(BootstrapUtils.class);
@ -45,6 +51,55 @@ abstract class BootstrapUtils {
/* no-op */ /* no-op */
} }
/**
* Create the {@code BootstrapContext} for the specified {@linkplain Class test class}.
*
* <p>Uses reflection to create a {@link org.springframework.test.context.support.DefaultBootstrapContext}
* that uses a {@link org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate}.
*
* @param testClass the test class for which the bootstrap context should be created
* @return a new {@code BootstrapContext}; never {@code null}
*/
@SuppressWarnings("unchecked")
static BootstrapContext createBootstrapContext(Class<?> testClass) {
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = createCacheAwareContextLoaderDelegate();
Class<? extends BootstrapContext> clazz = null;
try {
clazz = (Class<? extends BootstrapContext>) ClassUtils.forName(DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME,
BootstrapUtils.class.getClassLoader());
Constructor<? extends BootstrapContext> constructor = clazz.getConstructor(Class.class,
CacheAwareContextLoaderDelegate.class);
if (logger.isDebugEnabled()) {
logger.debug(String.format("Instantiating BootstrapContext using constructor [%s]", constructor));
}
return instantiateClass(constructor, testClass, cacheAwareContextLoaderDelegate);
}
catch (Throwable t) {
throw new IllegalStateException("Could not load BootstrapContext [" + clazz + "]", t);
}
}
@SuppressWarnings("unchecked")
private static CacheAwareContextLoaderDelegate createCacheAwareContextLoaderDelegate() {
Class<? extends CacheAwareContextLoaderDelegate> clazz = null;
try {
clazz = (Class<? extends CacheAwareContextLoaderDelegate>) ClassUtils.forName(
DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME, BootstrapUtils.class.getClassLoader());
if (logger.isDebugEnabled()) {
logger.debug(String.format("Instantiating CacheAwareContextLoaderDelegate from class [%s]",
clazz.getName()));
}
return instantiateClass(clazz, CacheAwareContextLoaderDelegate.class);
}
catch (Throwable t) {
throw new IllegalStateException("Could not load CacheAwareContextLoaderDelegate [" + clazz + "]", t);
}
}
/** /**
* Resolve the {@link TestContextBootstrapper} type for the test class in the * Resolve the {@link TestContextBootstrapper} type for the test class in the
* supplied {@link BootstrapContext}, instantiate it, and provide it a reference * supplied {@link BootstrapContext}, instantiate it, and provide it a reference

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain
* #loadContext loading} and {@linkplain #closeContext closing} application * #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. * the scenes.
* *
* <p>Note: {@code CacheAwareContextLoaderDelegate} does not extend the * <p>Note: {@code CacheAwareContextLoaderDelegate} does not extend the
@ -38,8 +38,10 @@ public interface CacheAwareContextLoaderDelegate {
* Load the {@linkplain ApplicationContext application context} for the supplied * Load the {@linkplain ApplicationContext application context} for the supplied
* {@link MergedContextConfiguration} by delegating to the {@link ContextLoader} * {@link MergedContextConfiguration} by delegating to the {@link ContextLoader}
* configured in the given {@code MergedContextConfiguration}. * 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. * be returned; otherwise, it will be loaded, stored in the cache, and returned.
* <p>The cache statistics should be logged by invoking
* {@link ContextCache#logStatistics()}.
* @param mergedContextConfiguration the merged context configuration to use * @param mergedContextConfiguration the merged context configuration to use
* to load the application context; never {@code null} * to load the application context; never {@code null}
* @return the application context * @return the application context
@ -50,7 +52,7 @@ public interface CacheAwareContextLoaderDelegate {
/** /**
* Remove the {@linkplain ApplicationContext application context} for the * 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 * and {@linkplain ConfigurableApplicationContext#close() close} it if it is
* an instance of {@link ConfigurableApplicationContext}. * an instance of {@link ConfigurableApplicationContext}.
* <p>The semantics of the supplied {@code HierarchyMode} must be honored when * <p>The semantics of the supplied {@code HierarchyMode} must be honored when

View File

@ -16,145 +16,55 @@
package org.springframework.test.context; 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.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.style.ToStringCreator;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode; 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 * {@code ContextCache} defines the public API for caching Spring
* environment. * {@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> * <h3>Rationale</h3>
* <p>Caching has significant performance benefits if initializing the context * <p>Context caching can have significant performance benefits if context
* takes a considerable about of time. Although initializing a Spring context * initialization is complex. So, although initializing a Spring context itself
* itself is very quick, some beans in a context, such as a * is typically very quick, some beans in a context &mdash; for example, an
* {@code LocalSessionFactoryBean} for working with Hibernate, may take some * in-memory database or a {@code LocalSessionFactoryBean} for working with
* time to initialize. Hence it often makes sense to perform that initialization * Hibernate &mdash; may take several seconds to initialize. Hence it often
* only once per test suite. * 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.
* *
* @author Sam Brannen * @author Sam Brannen
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 2.5 * @since 4.2
* @see ConcurrentReferenceHashMap
*/ */
class ContextCache { public interface ContextCache {
/** /**
* Map of context keys to Spring {@code ApplicationContext} instances. * The name of the logging category used for reporting {@code ContextCache}
* statistics.
*/ */
private final Map<MergedContextConfiguration, ApplicationContext> contextMap = public static final String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache";
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);
}
}
/** /**
* Determine whether there is a cached context for the given key. * Determine whether there is a cached context for the given key.
* @param key the context key (never {@code null}) * @param key the context key (never {@code null})
* @return {@code true} if the cache contains a context with the given key * @return {@code true} if the cache contains a context with the given key
*/ */
public boolean contains(MergedContextConfiguration key) { boolean contains(MergedContextConfiguration key);
Assert.notNull(key, "Key must not be null");
return this.contextMap.containsKey(key);
}
/** /**
* Obtain a cached {@code ApplicationContext} for the given key. * Obtain a cached {@code ApplicationContext} for the given key.
* <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will * <p>The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts
* be updated accordingly. * must be updated accordingly.
* @param key the context key (never {@code null}) * @param key the context key (never {@code null})
* @return the corresponding {@code ApplicationContext} instance, or {@code null} * @return the corresponding {@code ApplicationContext} instance, or {@code null}
* if not found in the cache * if not found in the cache
* @see #remove * @see #remove
*/ */
public ApplicationContext get(MergedContextConfiguration key) { 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();
}
/** /**
* Explicitly add an {@code ApplicationContext} instance to the cache * Explicitly add an {@code ApplicationContext} instance to the cache
@ -162,122 +72,79 @@ class ContextCache {
* @param key the context key (never {@code null}) * @param key the context key (never {@code null})
* @param context the {@code ApplicationContext} instance (never {@code null}) * @param context the {@code ApplicationContext} instance (never {@code null})
*/ */
public void put(MergedContextConfiguration key, ApplicationContext context) { 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();
}
}
/** /**
* Remove the context with the given key from the cache and explicitly * Remove the context with the given key from the cache and explicitly
* {@linkplain ConfigurableApplicationContext#close() close} it if it is an * {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close}
* instance of {@link ConfigurableApplicationContext}. * it if it is an instance of {@code ConfigurableApplicationContext}.
* <p>Generally speaking, you would only call this method if you change the * <p>Generally speaking, this method should be called if the state of
* state of a singleton bean, potentially affecting future interaction with * a singleton bean has been modified, potentially affecting future
* the context. * interaction with the context.
* <p>In addition, the semantics of the supplied {@code HierarchyMode} will * <p>In addition, the semantics of the supplied {@code HierarchyMode} must
* be honored. See the Javadoc for {@link HierarchyMode} for details. * be honored. See the Javadoc for {@link HierarchyMode} for details.
* @param key the context key; never {@code null} * @param key the context key; never {@code null}
* @param hierarchyMode the hierarchy mode; may be {@code null} if the context * @param hierarchyMode the hierarchy mode; may be {@code null} if the context
* is not part of a hierarchy * is not part of a hierarchy
*/ */
public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { 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);
}
/** /**
* Determine the number of contexts currently stored in the cache. * Determine the number of contexts currently stored in the cache.
* <p>If the cache contains more than {@code Integer.MAX_VALUE} elements, * <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() { int size();
return this.contextMap.size();
}
/** /**
* Determine the number of parent contexts currently tracked within the cache. * Determine the number of parent contexts currently tracked within the cache.
*/ */
public int getParentContextCount() { int getParentContextCount();
return this.hierarchyMap.size();
}
/** /**
* Generate a text string containing the statistics for this cache. * Get the overall hit count for this cache.
* <p>Specifically, the returned string contains the {@linkplain #size}, * <p>A <em>hit</em> is any access to the cache that returns a non-null
* {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() miss count}, * context for the queried key.
* and {@linkplain #getParentContextCount() parent context count}.
* @return the statistics for this cache
*/ */
@Override int getHitCount();
public String toString() {
return new ToStringCreator(this) /**
.append("size", size()) * Get the overall miss count for this cache.
.append("hitCount", getHitCount()) * <p>A <em>miss</em> is any access to the cache that returns a {@code null}
.append("missCount", getMissCount()) * context for the queried key.
.append("parentContextCount", getParentContextCount()) */
.toString(); 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();
/**
* Log the statistics for this {@code ContextCache} at {@code DEBUG} level
* using the {@value #CONTEXT_CACHE_LOGGING_CATEGORY} logging category.
* <p>The following information should be logged.
* <ul>
* <li>name of the concrete {@code ContextCache} implementation</li>
* <li>{@linkplain #size}</li>
* <li>{@linkplain #getParentContextCount() parent context count}</li>
* <li>{@linkplain #getHitCount() hit count}</li>
* <li>{@linkplain #getMissCount() miss count}</li>
* <li>any other information useful for monitoring the state of this cache</li>
* </ul>
*/
void logStatistics();
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,25 +19,33 @@ package org.springframework.test.context;
import java.util.List; import java.util.List;
/** /**
* {@code TestContextBootstrapper} defines a strategy SPI for bootstrapping the * {@code TestContextBootstrapper} defines the SPI for bootstrapping the
* <em>Spring TestContext Framework</em>. * <em>Spring TestContext Framework</em>.
* *
* <p>A custom bootstrapping strategy can be configured for a test class via * <p>A {@code TestContextBootstrapper} is used by the {@link TestContextManager} to
* {@link BootstrapWith @BootstrapWith}, either directly or as a meta-annotation. * {@linkplain #getTestExecutionListeners get the TestExecutionListeners} for the
* See {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} * current test and to {@linkplain #buildTestContext build the TestContext} that
* it manages.
*
* <h3>Configuration</h3>
*
* <p>A custom bootstrapping strategy can be configured for a test class (or
* test class hierarchy) via {@link BootstrapWith @BootstrapWith}, either
* directly or as a meta-annotation. See
* {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}
* for an example. * for an example.
* *
* <p>The {@link TestContextManager} uses a {@code TestContextBootstrapper} to * <p>If a bootstrapper is not explicitly configured via {@code @BootstrapWith}, the
* {@linkplain #getTestExecutionListeners get the TestExecutionListeners} for the * {@link org.springframework.test.context.support.DefaultTestContextBootstrapper DefaultTestContextBootstrapper}
* current test and to {@linkplain #buildMergedContextConfiguration build the * will be used.
* merged context configuration} necessary to create the {@link TestContext} that *
* it manages. * <h3>Implementation Notes</h3>
* *
* <p>Concrete implementations must provide a {@code public} no-args constructor. * <p>Concrete implementations must provide a {@code public} no-args constructor.
* *
* <p><strong>Note</strong>: this SPI might potentially change in the future in * <p><strong>WARNING</strong>: this SPI will likely change in the future in
* order to accommodate new requirements. Implementers are therefore strongly encouraged * order to accommodate new requirements. Implementers are therefore strongly encouraged
* <em>not</em> to implement this interface directly but rather to <em>extend</em> * <strong>not</strong> to implement this interface directly but rather to <em>extend</em>
* {@link org.springframework.test.context.support.AbstractTestContextBootstrapper * {@link org.springframework.test.context.support.AbstractTestContextBootstrapper
* AbstractTestContextBootstrapper} or one of its concrete subclasses instead. * AbstractTestContextBootstrapper} or one of its concrete subclasses instead.
* *
@ -58,6 +66,46 @@ public interface TestContextBootstrapper {
*/ */
BootstrapContext getBootstrapContext(); BootstrapContext getBootstrapContext();
/**
* Build the {@link TestContext} for the {@link BootstrapContext}
* associated with this bootstrapper.
* @return a new {@link TestContext}, never {@code null}
* @since 4.2
* @see #buildMergedContextConfiguration()
*/
TestContext buildTestContext();
/**
* Build the {@linkplain MergedContextConfiguration merged context configuration}
* for the test class in the {@link BootstrapContext} associated with this
* bootstrapper.
* <p>Implementations must take the following into account when building the
* merged configuration:
* <ul>
* <li>Context hierarchies declared via {@link ContextHierarchy @ContextHierarchy}
* and {@link ContextConfiguration @ContextConfiguration}</li>
* <li>Active bean definition profiles declared via {@link ActiveProfiles @ActiveProfiles}</li>
* <li>{@linkplain org.springframework.context.ApplicationContextInitializer
* Context initializers} declared via {@link ContextConfiguration#initializers}</li>
* <li>Test property sources declared via {@link TestPropertySource @TestPropertySource}</li>
* </ul>
* <p>Consult the Javadoc for the aforementioned annotations for details on
* the required semantics.
* <p>Note that the implementation of {@link #buildTestContext()} should
* typically delegate to this method when constructing the {@code TestContext}.
* <p>When determining which {@link ContextLoader} to use for a given test
* class, the following algorithm should be used:
* <ol>
* <li>If a {@code ContextLoader} class has been explicitly declared via
* {@link ContextConfiguration#loader}, use it.</li>
* <li>Otherwise, concrete implementations are free to determine which
* {@code ContextLoader} class to use as as default.</li>
* </ol>
* @return the merged context configuration, never {@code null}
* @see #buildTestContext()
*/
MergedContextConfiguration buildMergedContextConfiguration();
/** /**
* Get a list of newly instantiated {@link TestExecutionListener TestExecutionListeners} * Get a list of newly instantiated {@link TestExecutionListener TestExecutionListeners}
* for the test class in the {@link BootstrapContext} associated with this bootstrapper. * for the test class in the {@link BootstrapContext} associated with this bootstrapper.
@ -82,31 +130,4 @@ public interface TestContextBootstrapper {
*/ */
List<TestExecutionListener> getTestExecutionListeners(); List<TestExecutionListener> getTestExecutionListeners();
/**
* Build the {@linkplain MergedContextConfiguration merged context configuration}
* for the test class in the {@link BootstrapContext} associated with this
* bootstrapper.
* <p>Implementations must take the following into account when building the
* merged configuration:
* <ul>
* <li>Context hierarchies declared via {@link ContextHierarchy @ContextHierarchy}
* and {@link ContextConfiguration @ContextConfiguration}</li>
* <li>Active bean definition profiles declared via {@link ActiveProfiles @ActiveProfiles}</li>
* <li>{@linkplain org.springframework.context.ApplicationContextInitializer
* Context initializers} declared via {@link ContextConfiguration#initializers}</li>
* </ul>
* <p>Consult the Javadoc for the aforementioned annotations for details on
* the required semantics.
* <p>When determining which {@link ContextLoader} to use for a given test
* class, the following algorithm should be used:
* <ol>
* <li>If a {@code ContextLoader} class has been explicitly declared via
* {@link ContextConfiguration#loader}, use it.</li>
* <li>Otherwise, concrete implementations are free to determine which
* {@code ContextLoader} class to use as as default.</li>
* </ol>
* @return the merged context configuration, never {@code null}
*/
MergedContextConfiguration buildMergedContextConfiguration();
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,15 +28,11 @@ import org.springframework.util.Assert;
/** /**
* {@code TestContextManager} is the main entry point into the <em>Spring * {@code TestContextManager} is the main entry point into the <em>Spring
* TestContext Framework</em>, which provides support for loading and accessing * TestContext Framework</em>.
* {@link org.springframework.context.ApplicationContext application contexts},
* dependency injection of test instances,
* {@link org.springframework.transaction.annotation.Transactional transactional}
* execution of test methods, etc.
* *
* <p>Specifically, a {@code TestContextManager} is responsible for managing a * <p>Specifically, a {@code TestContextManager} is responsible for managing a
* single {@link TestContext} and signaling events to all registered * single {@link TestContext} and signaling events to all registered
* {@link TestExecutionListener TestExecutionListeners} at well defined test * {@link TestExecutionListener TestExecutionListeners} at the following test
* execution points: * execution points:
* *
* <ul> * <ul>
@ -56,6 +52,21 @@ import org.springframework.util.Assert;
* 4's {@link org.junit.AfterClass @AfterClass})</li> * 4's {@link org.junit.AfterClass @AfterClass})</li>
* </ul> * </ul>
* *
* <p>Support for loading and accessing
* {@link org.springframework.context.ApplicationContext application contexts},
* dependency injection of test instances,
* {@link org.springframework.transaction.annotation.Transactional transactional}
* execution of test methods, etc. is provided by
* {@link SmartContextLoader ContextLoaders} and {@link TestExecutionListener
* TestExecutionListeners}, which are configured via
* {@link ContextConfiguration @ContextConfiguration} and
* {@link TestExecutionListeners @TestExecutionListeners}.
*
* <p>Bootstrapping of the {@code TestContext}, the default {@code ContextLoader},
* default {@code TestExecutionListeners}, and their collaborators is performed
* by a {@link TestContextBootstrapper}, which is configured via
* {@link BootstrapWith @BootstrapWith}.
*
* @author Sam Brannen * @author Sam Brannen
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 2.5 * @since 2.5
@ -73,37 +84,42 @@ public class TestContextManager {
private static final Log logger = LogFactory.getLog(TestContextManager.class); private static final Log logger = LogFactory.getLog(TestContextManager.class);
/**
* Cache of Spring application contexts.
* <p>This needs to be static, since test instances may be destroyed and
* recreated between invocations of individual test methods, as is the case
* with JUnit.
*/
static final ContextCache contextCache = new ContextCache();
private final TestContext testContext; private final TestContext testContext;
private final TestContextBootstrapper testContextBootstrapper;
private final List<TestExecutionListener> testExecutionListeners = new ArrayList<TestExecutionListener>(); private final List<TestExecutionListener> testExecutionListeners = new ArrayList<TestExecutionListener>();
/** /**
* Construct a new {@code TestContextManager} for the specified {@linkplain Class test class} * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class},
* and automatically {@link #registerTestExecutionListeners register} the * automatically {@linkplain #registerTestExecutionListeners registering} the necessary
* {@link TestExecutionListener TestExecutionListeners} configured for the test class * {@link TestExecutionListener TestExecutionListeners}.
* via the {@link TestExecutionListeners @TestExecutionListeners} annotation. * <p>Delegates to a {@link TestContextBootstrapper} for building the {@code TestContext}
* and retrieving the {@code TestExecutionListeners}.
* @param testClass the test class to be managed * @param testClass the test class to be managed
* @see TestContextBootstrapper#buildTestContext
* @see TestContextBootstrapper#getTestExecutionListeners
* @see #registerTestExecutionListeners * @see #registerTestExecutionListeners
*/ */
public TestContextManager(Class<?> testClass) { public TestContextManager(Class<?> testClass) {
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(contextCache); BootstrapContext bootstrapContext = createBootstrapContext(testClass);
BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); TestContextBootstrapper bootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext);
this.testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); this.testContext = bootstrapper.buildTestContext();
this.testContext = new DefaultTestContext(this.testContextBootstrapper); registerTestExecutionListeners(bootstrapper.getTestExecutionListeners());
registerTestExecutionListeners(this.testContextBootstrapper.getTestExecutionListeners());
} }
/**
* Create the {@code BootstrapContext} for the specified {@linkplain Class test class}.
* <p>The default implementation creates a
* {@link org.springframework.test.context.support.DefaultBootstrapContext DefaultBootstrapContext}
* that uses a
* {@link org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate DefaultCacheAwareContextLoaderDelegate}.
* <p>Can be overridden by subclasses as necessary.
* @param testClass the test class for which the bootstrap context should be created
* @return a new {@code BootstrapContext}; never {@code null}
*/
protected BootstrapContext createBootstrapContext(Class<?> testClass) {
return BootstrapUtils.createBootstrapContext(testClass);
}
/** /**
* Get the {@link TestContext} managed by this {@code TestContextManager}. * Get the {@link TestContext} managed by this {@code TestContextManager}.
@ -201,7 +217,7 @@ public class TestContextManager {
* @see #getTestExecutionListeners() * @see #getTestExecutionListeners()
*/ */
public void prepareTestInstance(Object testInstance) throws Exception { public void prepareTestInstance(Object testInstance) throws Exception {
Assert.notNull(testInstance, "testInstance must not be null"); Assert.notNull(testInstance, "Test instance must not be null");
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("prepareTestInstance(): instance [" + testInstance + "]"); logger.trace("prepareTestInstance(): instance [" + testInstance + "]");
} }
@ -282,7 +298,7 @@ public class TestContextManager {
* @see #getTestExecutionListeners() * @see #getTestExecutionListeners()
*/ */
public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception { public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception {
Assert.notNull(testInstance, "testInstance must not be null"); Assert.notNull(testInstance, "Test instance must not be null");
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod + logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod +
"], exception [" + exception + "]"); "], exception [" + exception + "]");

View File

@ -38,12 +38,14 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.BootstrapContext;
import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextCache;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.ContextLoader; import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader; import org.springframework.test.context.SmartContextLoader;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.TestExecutionListeners;
@ -66,6 +68,9 @@ import org.springframework.util.StringUtils;
* <li>{@link #processMergedContextConfiguration} * <li>{@link #processMergedContextConfiguration}
* </ul> * </ul>
* *
* <p>To plug in custom {@link ContextCache} support, override
* {@link #getCacheAwareContextLoaderDelegate()}.
*
* @author Sam Brannen * @author Sam Brannen
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 4.1 * @since 4.1
@ -93,6 +98,21 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
return this.bootstrapContext; return this.bootstrapContext;
} }
/**
* Build a new {@link DefaultTestContext} using the {@linkplain Class test class}
* in the {@link BootstrapContext} associated with this bootstrapper and
* by delegating to {@link #buildMergedContextConfiguration()} and
* {@link #getCacheAwareContextLoaderDelegate()}.
* <p>Concrete subclasses may choose to override this method to return a
* custom {@link TestContext} implementation.
* @since 4.2
*/
@Override
public TestContext buildTestContext() {
return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
getCacheAwareContextLoaderDelegate());
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -266,7 +286,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
@Override @Override
public final MergedContextConfiguration buildMergedContextConfiguration() { public final MergedContextConfiguration buildMergedContextConfiguration() {
Class<?> testClass = getBootstrapContext().getTestClass(); Class<?> testClass = getBootstrapContext().getTestClass();
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getBootstrapContext().getCacheAwareContextLoaderDelegate(); CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();
if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class, if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class,
ContextHierarchy.class) == null) { ContextHierarchy.class) == null) {
@ -455,6 +475,20 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
return null; return null;
} }
/**
* Get the {@link CacheAwareContextLoaderDelegate} to use for transparent
* interaction with the {@code ContextCache}.
* <p>The default implementation simply delegates to
* {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()}.
* <p>Concrete subclasses may choose to override this method to return a
* custom {@code CacheAwareContextLoaderDelegate} implementation with
* custom {@link ContextCache} support.
* @return the context loader delegate (never {@code null})
*/
protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() {
return getBootstrapContext().getCacheAwareContextLoaderDelegate();
}
/** /**
* Determine the default {@link ContextLoader} {@linkplain Class class} * Determine the default {@link ContextLoader} {@linkplain Class class}
* to use for the supplied test class. * to use for the supplied test class.

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,9 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context; package org.springframework.test.context.support;
import org.springframework.core.style.ToStringCreator; import org.springframework.core.style.ToStringCreator;
import org.springframework.test.context.BootstrapContext;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -25,13 +27,19 @@ import org.springframework.util.Assert;
* @author Sam Brannen * @author Sam Brannen
* @since 4.1 * @since 4.1
*/ */
class DefaultBootstrapContext implements BootstrapContext { public class DefaultBootstrapContext implements BootstrapContext {
private final Class<?> testClass; private final Class<?> testClass;
private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
DefaultBootstrapContext(Class<?> testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { /**
* Construct a new {@code DefaultBootstrapContext} from the supplied arguments.
* @param testClass the test class for this bootstrap context; never {@code null}
* @param cacheAwareContextLoaderDelegate the context loader delegate to use for
* transparent interaction with the {@code ContextCache}; never {@code null}
*/
public DefaultBootstrapContext(Class<?> testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
Assert.notNull(testClass, "Test class must not be null"); Assert.notNull(testClass, "Test class must not be null");
Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null"); Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null");
this.testClass = testClass; this.testClass = testClass;

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,47 +14,78 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context; package org.springframework.test.context.support;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.ContextCache;
import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* Default implementation of the {@link CacheAwareContextLoaderDelegate} interface. * Default implementation of the {@link CacheAwareContextLoaderDelegate} interface.
* *
* <p>Although {@code DefaultCacheAwareContextLoaderDelegate} was first introduced * <p>To use a static {@code DefaultContextCache}, invoke the
* in Spring Framework 4.1, the initial implementation of this class was extracted * {@link #DefaultCacheAwareContextLoaderDelegate()} constructor; otherwise,
* from the existing code base for {@code CacheAwareContextLoaderDelegate} when * invoke the {@link #DefaultCacheAwareContextLoaderDelegate(ContextCache)}
* {@code CacheAwareContextLoaderDelegate} was converted into an interface. * and provide a custom {@link ContextCache} implementation.
* *
* @author Sam Brannen * @author Sam Brannen
* @since 4.1 * @since 4.1
*/ */
class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate { public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate {
private static final Log logger = LogFactory.getLog(DefaultCacheAwareContextLoaderDelegate.class); private static final Log logger = LogFactory.getLog(DefaultCacheAwareContextLoaderDelegate.class);
private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache"); /**
* Default static cache of Spring application contexts.
*/
static final ContextCache defaultContextCache = new DefaultContextCache();
private final ContextCache contextCache; private final ContextCache contextCache;
DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { /**
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
* a static {@link DefaultContextCache}.
* <p>This default cache is static so that each context can be cached
* and reused for all subsequent tests that declare the same unique
* context configuration within the same JVM process.
* @see #DefaultCacheAwareContextLoaderDelegate(ContextCache)
*/
public DefaultCacheAwareContextLoaderDelegate() {
this(defaultContextCache);
}
/**
* Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using
* the supplied {@link ContextCache}.
* @see #DefaultCacheAwareContextLoaderDelegate()
*/
public DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) {
Assert.notNull(contextCache, "ContextCache must not be null"); Assert.notNull(contextCache, "ContextCache must not be null");
this.contextCache = contextCache; this.contextCache = contextCache;
} }
/**
* Get the {@link ContextCache} used by this context loader delegate.
*/
protected ContextCache getContextCache() {
return this.contextCache;
}
/** /**
* Load the {@code ApplicationContext} for the supplied merged context configuration. * Load the {@code ApplicationContext} for the supplied merged context configuration.
* <p>Supports both the {@link SmartContextLoader} and {@link ContextLoader} SPIs. * <p>Supports both the {@link SmartContextLoader} and {@link ContextLoader} SPIs.
* @throws Exception if an error occurs while loading the application context * @throws Exception if an error occurs while loading the application context
*/ */
private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration)
throws Exception { throws Exception {
ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); ContextLoader contextLoader = mergedContextConfiguration.getContextLoader();
@ -101,9 +132,7 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD
} }
} }
if (statsLogger.isDebugEnabled()) { this.contextCache.logStatistics();
statsLogger.debug("Spring test ApplicationContext cache statistics: " + this.contextCache);
}
return context; return context;
} }

View File

@ -0,0 +1,270 @@
/*
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 {
private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY);
/**
* 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 void logStatistics() {
if (statsLogger.isDebugEnabled()) {
statsLogger.debug("Spring test ApplicationContext cache statistics: " + this);
}
}
/**
* Generate a text string containing the implementation type of this
* cache and its statistics.
* <p>The string returned by this method contains all information
* required for compliance with the contract for {@link #logStatistics()}.
* @return a string representation of this cache, including statistics
*/
@Override
public String toString() {
return new ToStringCreator(this)
.append("size", size())
.append("parentContextCount", getParentContextCount())
.append("hitCount", getHitCount())
.append("missCount", getMissCount())
.toString();
}
}

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context; package org.springframework.test.context.support;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -22,21 +22,19 @@ import org.springframework.context.ApplicationContext;
import org.springframework.core.AttributeAccessorSupport; import org.springframework.core.AttributeAccessorSupport;
import org.springframework.core.style.ToStringCreator; import org.springframework.core.style.ToStringCreator;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* Default implementation of the {@link TestContext} interface. * Default implementation of the {@link TestContext} interface.
* *
* <p>Although {@code DefaultTestContext} was first introduced in Spring Framework
* 4.0, the initial implementation of this class was extracted from the existing
* code base for {@code TestContext} when {@code TestContext} was converted into
* an interface.
*
* @author Sam Brannen * @author Sam Brannen
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 4.0 * @since 4.0
*/ */
class DefaultTestContext extends AttributeAccessorSupport implements TestContext { public class DefaultTestContext extends AttributeAccessorSupport implements TestContext {
private static final long serialVersionUID = -5827157174866681233L; private static final long serialVersionUID = -5827157174866681233L;
@ -54,24 +52,42 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext
/** /**
* Construct a new test context using the supplied {@link TestContextBootstrapper}. * Construct a new {@code DefaultTestContext} from the supplied arguments.
* @param testContextBootstrapper the {@code TestContextBootstrapper} to use * @param testClass the test class for this test context; never {@code null}
* to construct the test context (must not be {@code null}) * @param mergedContextConfiguration the merged application context
* configuration for this test context; never {@code null}
* @param cacheAwareContextLoaderDelegate the delegate to use for loading
* and closing the application context for this test context; never {@code null}
*/ */
DefaultTestContext(TestContextBootstrapper testContextBootstrapper) { public DefaultTestContext(Class<?> testClass, MergedContextConfiguration mergedContextConfiguration,
Assert.notNull(testContextBootstrapper, "TestContextBootstrapper must not be null"); CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
Assert.notNull(testClass, "testClass must not be null");
BootstrapContext bootstrapContext = testContextBootstrapper.getBootstrapContext(); Assert.notNull(mergedContextConfiguration, "MergedContextConfiguration must not be null");
this.testClass = bootstrapContext.getTestClass(); Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null");
this.cacheAwareContextLoaderDelegate = bootstrapContext.getCacheAwareContextLoaderDelegate(); this.testClass = testClass;
this.mergedContextConfiguration = testContextBootstrapper.buildMergedContextConfiguration(); this.mergedContextConfiguration = mergedContextConfiguration;
this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate;
} }
/**
* Get the {@linkplain ApplicationContext application context} for this
* test context.
* <p>The default implementation delegates to the {@link CacheAwareContextLoaderDelegate}
* that was supplied when this {@code TestContext} was constructed.
* @see CacheAwareContextLoaderDelegate#loadContext
*/
public ApplicationContext getApplicationContext() { public ApplicationContext getApplicationContext() {
return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration); return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
} }
/**
* Mark the {@linkplain ApplicationContext application context} associated
* with this test context as <em>dirty</em> (i.e., by removing it from the
* context cache and closing it).
* <p>The default implementation delegates to the {@link CacheAwareContextLoaderDelegate}
* that was supplied when this {@code TestContext} was constructed.
* @see CacheAwareContextLoaderDelegate#closeContext
*/
public void markApplicationContextDirty(HierarchyMode hierarchyMode) { public void markApplicationContextDirty(HierarchyMode hierarchyMode) {
this.cacheAwareContextLoaderDelegate.closeContext(this.mergedContextConfiguration, hierarchyMode); this.cacheAwareContextLoaderDelegate.closeContext(this.mergedContextConfiguration, hierarchyMode);
} }

View File

@ -16,6 +16,8 @@
package org.springframework.test.context; package org.springframework.test.context;
import org.springframework.test.context.support.DefaultBootstrapContext;
/** /**
* Collection of test-related utility methods for working with {@link BootstrapContext * Collection of test-related utility methods for working with {@link BootstrapContext
* BootstrapContexts} and {@link TestContextBootstrapper TestContextBootstrappers}. * BootstrapContexts} and {@link TestContextBootstrapper TestContextBootstrappers}.

View File

@ -37,7 +37,7 @@ import org.springframework.test.context.testng.TrackingTestNGTestListener;
import org.testng.TestNG; import org.testng.TestNG;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.test.context.ContextCacheTestUtils.*; import static org.springframework.test.context.support.ContextCacheTestUtils.*;
/** /**
* JUnit 4 based integration test which verifies correct {@linkplain ContextCache * JUnit 4 based integration test which verifies correct {@linkplain ContextCache
@ -79,8 +79,9 @@ public class ClassLevelDirtiesContextTestNGTests {
@BeforeClass @BeforeClass
public static void verifyInitialCacheState() { public static void verifyInitialCacheState() {
ContextCache contextCache = TestContextManager.contextCache; resetContextCache();
contextCache.reset(); // Reset static counters in case tests are run multiple times in a test suite --
// for example, via JUnit's @Suite.
cacheHits.set(0); cacheHits.set(0);
cacheMisses.set(0); cacheMisses.set(0);
assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());

View File

@ -36,7 +36,7 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.test.context.ContextCacheTestUtils.*; import static org.springframework.test.context.support.ContextCacheTestUtils.*;
/** /**
* JUnit 4 based integration test which verifies correct {@linkplain ContextCache * JUnit 4 based integration test which verifies correct {@linkplain ContextCache
@ -74,8 +74,9 @@ public class ClassLevelDirtiesContextTests {
@BeforeClass @BeforeClass
public static void verifyInitialCacheState() { public static void verifyInitialCacheState() {
ContextCache contextCache = TestContextManager.contextCache; resetContextCache();
contextCache.reset(); // Reset static counters in case tests are run multiple times in a test suite --
// for example, via JUnit's @Suite.
cacheHits.set(0); cacheHits.set(0);
cacheMisses.set(0); cacheMisses.set(0);
assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());

View File

@ -23,10 +23,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.support.AnnotationConfigContextLoader; import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.test.context.support.DefaultContextCache;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.test.context.ContextCacheTestUtils.*; import static org.springframework.test.context.support.ContextCacheTestUtils.*;
/** /**
* Integration tests for verifying proper behavior of the {@link ContextCache} in * Integration tests for verifying proper behavior of the {@link ContextCache} in
@ -39,7 +40,7 @@ import static org.springframework.test.context.ContextCacheTestUtils.*;
*/ */
public class ContextCacheTests { public class ContextCacheTests {
private ContextCache contextCache = new ContextCache(); private ContextCache contextCache = new DefaultContextCache();
@Before @Before

View File

@ -31,7 +31,7 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.springframework.test.context.ContextCacheTestUtils.*; import static org.springframework.test.context.support.ContextCacheTestUtils.*;
/** /**
* JUnit 4 based unit test which verifies correct {@link ContextCache * JUnit 4 based unit test which verifies correct {@link ContextCache
@ -58,8 +58,7 @@ public class SpringRunnerContextCacheTests {
@BeforeClass @BeforeClass
public static void verifyInitialCacheState() { public static void verifyInitialCacheState() {
dirtiedApplicationContext = null; dirtiedApplicationContext = null;
ContextCache contextCache = TestContextManager.contextCache; resetContextCache();
contextCache.reset();
assertContextCacheStatistics("BeforeClass", 0, 0, 0); assertContextCacheStatistics("BeforeClass", 0, 0, 0);
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
package org.springframework.test.context; package org.springframework.test.context;
import org.springframework.test.context.support.DefaultBootstrapContext;
import org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate;
/** /**
* Collection of test-related utility methods for working with {@link TestContext TestContexts}. * Collection of test-related utility methods for working with {@link TestContext TestContexts}.
* *
@ -28,12 +31,12 @@ public abstract class TestContextTestUtils {
return buildTestContext(testClass, new DefaultCacheAwareContextLoaderDelegate(contextCache)); return buildTestContext(testClass, new DefaultCacheAwareContextLoaderDelegate(contextCache));
} }
public static TestContext buildTestContext( public static TestContext buildTestContext(Class<?> testClass,
Class<?> testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate);
TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext);
return new DefaultTestContext(testContextBootstrapper); return testContextBootstrapper.buildTestContext();
} }
} }

View File

@ -14,7 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.test.context; package org.springframework.test.context.support;
import org.springframework.test.context.ContextCache;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -28,7 +30,14 @@ import static org.junit.Assert.*;
public class ContextCacheTestUtils { public class ContextCacheTestUtils {
/** /**
* Assert the statistics of the context cache in {@link TestContextManager}. * Reset the state of the static context cache in {@link DefaultCacheAwareContextLoaderDelegate}.
*/
public static final void resetContextCache() {
DefaultCacheAwareContextLoaderDelegate.defaultContextCache.reset();
}
/**
* Assert the statistics of the static context cache in {@link DefaultCacheAwareContextLoaderDelegate}.
* *
* @param usageScenario the scenario in which the statistics are used * @param usageScenario the scenario in which the statistics are used
* @param expectedSize the expected number of contexts in the cache * @param expectedSize the expected number of contexts in the cache
@ -37,8 +46,8 @@ public class ContextCacheTestUtils {
*/ */
public static final void assertContextCacheStatistics(String usageScenario, int expectedSize, int expectedHitCount, public static final void assertContextCacheStatistics(String usageScenario, int expectedSize, int expectedHitCount,
int expectedMissCount) { int expectedMissCount) {
assertContextCacheStatistics(TestContextManager.contextCache, usageScenario, expectedSize, expectedHitCount, assertContextCacheStatistics(DefaultCacheAwareContextLoaderDelegate.defaultContextCache, usageScenario,
expectedMissCount); expectedSize, expectedHitCount, expectedMissCount);
} }
/** /**