diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
index f7375def035..507d2555741 100644
--- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
+++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2014 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.
@@ -18,6 +18,7 @@ package org.springframework.test.context;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
@@ -37,6 +38,8 @@ public class CacheAwareContextLoaderDelegate {
private static final Log logger = LogFactory.getLog(CacheAwareContextLoaderDelegate.class);
+ private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache");
+
private final ContextCache contextCache;
@@ -45,6 +48,7 @@ public class CacheAwareContextLoaderDelegate {
this.contextCache = contextCache;
}
+
/**
* Load the {@code ApplicationContext} for the supplied merged context
* configuration. Supports both the {@link SmartContextLoader} and
@@ -53,9 +57,10 @@ public class CacheAwareContextLoaderDelegate {
*/
private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration)
throws Exception {
+
ContextLoader contextLoader = mergedContextConfiguration.getContextLoader();
- Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. "
- + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
+ Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " +
+ "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
ApplicationContext applicationContext;
@@ -65,8 +70,8 @@ public class CacheAwareContextLoaderDelegate {
}
else {
String[] locations = mergedContextConfiguration.getLocations();
- Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. "
- + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
+ Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " +
+ "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
applicationContext = contextLoader.loadContext(locations);
}
@@ -76,7 +81,6 @@ public class CacheAwareContextLoaderDelegate {
/**
* Load the {@link ApplicationContext application context} for the supplied
* merged context configuration.
- *
*
If the context is present in the cache it will simply be returned;
* otherwise, it will be loaded, stored in the cache, and returned.
* @return the application context
@@ -84,16 +88,16 @@ public class CacheAwareContextLoaderDelegate {
* loading the application context
*/
public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) {
- synchronized (contextCache) {
- ApplicationContext context = contextCache.get(mergedContextConfiguration);
+ synchronized (this.contextCache) {
+ ApplicationContext context = this.contextCache.get(mergedContextConfiguration);
if (context == null) {
try {
context = loadContextInternal(mergedContextConfiguration);
if (logger.isDebugEnabled()) {
- logger.debug(String.format("Storing ApplicationContext in cache under key [%s].",
- mergedContextConfiguration));
+ logger.debug(String.format("Storing ApplicationContext in cache under key [%s]",
+ mergedContextConfiguration));
}
- contextCache.put(mergedContextConfiguration, context);
+ this.contextCache.put(mergedContextConfiguration, context);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load ApplicationContext", ex);
@@ -101,10 +105,15 @@ public class CacheAwareContextLoaderDelegate {
}
else {
if (logger.isDebugEnabled()) {
- logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s].",
- mergedContextConfiguration));
+ logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s]",
+ mergedContextConfiguration));
}
}
+
+ if (statsLogger.isDebugEnabled()) {
+ statsLogger.debug("Spring test ApplicationContext cache statistics: " + this.contextCache);
+ }
+
return context;
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java
index 3a5a5b1adbd..576021bdaa5 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2014 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.
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
@@ -47,13 +48,11 @@ import org.springframework.util.Assert;
*/
class ContextCache {
- private final Object monitor = new Object();
-
/**
* Map of context keys to Spring {@code ApplicationContext} instances.
*/
- private final Map contextMap = new ConcurrentHashMap(
- 64);
+ private final Map contextMap =
+ new ConcurrentHashMap(64);
/**
* Map of parent keys to sets of children keys, representing a top-down tree
@@ -61,128 +60,99 @@ class ContextCache {
* need to be recursively removed and closed when removing a context that is a parent
* of other contexts.
*/
- private final Map> hierarchyMap = new ConcurrentHashMap>(
- 64);
+ private final Map> hierarchyMap =
+ new ConcurrentHashMap>(64);
- private int hitCount;
+ private final AtomicInteger hitCount = new AtomicInteger();
- private int missCount;
+ private final AtomicInteger missCount = new AtomicInteger();
/**
- * Clears all contexts from the cache and clears context hierarchy information as
- * well.
+ * Clear all contexts from the cache and clears context hierarchy information as well.
*/
- void clear() {
- synchronized (monitor) {
- this.contextMap.clear();
- this.hierarchyMap.clear();
- }
+ public void clear() {
+ this.contextMap.clear();
+ this.hierarchyMap.clear();
}
/**
- * Clears hit and miss count statistics for the cache (i.e., resets counters to zero).
+ * Clear hit and miss count statistics for the cache (i.e., resets counters to zero).
*/
- void clearStatistics() {
- this.hitCount = 0;
- this.missCount = 0;
+ public void clearStatistics() {
+ this.hitCount.set(0);
+ this.missCount.set(0);
}
/**
* Return whether there is a cached context for the given key.
- *
* @param key the context key (never {@code null})
*/
- boolean contains(MergedContextConfiguration key) {
+ public boolean contains(MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
- synchronized (monitor) {
- return this.contextMap.containsKey(key);
- }
+ return this.contextMap.containsKey(key);
}
/**
* Obtain a cached {@code ApplicationContext} for the given key.
- *
- * The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will be
- * updated accordingly.
- *
+ *
The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will
+ * 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.
+ * @return the corresponding {@code ApplicationContext} instance, or {@code null}
+ * if not found in the cache
* @see #remove
*/
- ApplicationContext get(MergedContextConfiguration key) {
+ public ApplicationContext get(MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
- synchronized (monitor) {
- ApplicationContext context = this.contextMap.get(key);
- if (context == null) {
- incrementMissCount();
- }
- else {
- incrementHitCount();
- }
- return context;
+ ApplicationContext context = this.contextMap.get(key);
+ if (context == null) {
+ this.missCount.incrementAndGet();
}
+ else {
+ this.hitCount.incrementAndGet();
+ }
+ return context;
}
/**
- * Increment the hit count by one. A hit is an access to the cache, which
- * returned a non-null context for a queried key.
- */
- private void incrementHitCount() {
- this.hitCount++;
- }
-
- /**
- * Increment the miss count by one. A miss is an access to the cache, which
- * returned a {@code null} context for a queried key.
- */
- private void incrementMissCount() {
- this.missCount++;
- }
-
- /**
- * Get the overall hit count for this cache. A hit is an access to the cache,
- * which returned a non-null context for a queried key.
+ * Get the overall hit count for this cache.
+ *
A hit is an access to the cache, which returned a non-null context
+ * for a queried key.
*/
- int getHitCount() {
- return this.hitCount;
+ public int getHitCount() {
+ return this.hitCount.get();
}
/**
- * Get the overall miss count for this cache. A miss is an access to the
- * cache, which returned a {@code null} context for a queried key.
+ * Get the overall miss count for this cache.
+ *
A miss is an access to the cache, which returned a {@code null} context
+ * for a queried key.
*/
- int getMissCount() {
- return this.missCount;
+ public int getMissCount() {
+ return this.missCount.get();
}
/**
- * Explicitly add an {@code ApplicationContext} instance to the cache under the given
- * key.
- *
+ * Explicitly add an {@code ApplicationContext} instance to the cache under the given key.
* @param key the context key (never {@code null})
* @param context the {@code ApplicationContext} instance (never {@code null})
*/
- void put(MergedContextConfiguration key, ApplicationContext context) {
+ public void put(MergedContextConfiguration key, ApplicationContext context) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(context, "ApplicationContext must not be null");
- synchronized (monitor) {
- this.contextMap.put(key, context);
-
- MergedContextConfiguration child = key;
- MergedContextConfiguration parent = child.getParent();
- while (parent != null) {
- Set list = hierarchyMap.get(parent);
- if (list == null) {
- list = new HashSet();
- hierarchyMap.put(parent, list);
- }
- list.add(child);
- child = parent;
- parent = child.getParent();
+ this.contextMap.put(key, context);
+ MergedContextConfiguration child = key;
+ MergedContextConfiguration parent = child.getParent();
+ while (parent != null) {
+ Set list = this.hierarchyMap.get(parent);
+ if (list == null) {
+ list = new HashSet();
+ this.hierarchyMap.put(parent, list);
}
+ list.add(child);
+ child = parent;
+ parent = child.getParent();
}
}
@@ -190,19 +160,16 @@ class ContextCache {
* 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}.
- *
* Generally speaking, you would only call this method if you change the
* state of a singleton bean, potentially affecting future interaction with
* the context.
- *
*
In addition, the semantics of the supplied {@code HierarchyMode} will
* be honored. See the Javadoc for {@link HierarchyMode} for details.
- *
* @param key the context key; never {@code null}
* @param hierarchyMode the hierarchy mode; may be {@code null} if the context
* is not part of a hierarchy
*/
- void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) {
+ 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
@@ -214,24 +181,21 @@ class ContextCache {
}
}
- synchronized (monitor) {
- final List removedContexts = new ArrayList();
-
- remove(removedContexts, startKey);
+ List removedContexts = new ArrayList();
+ remove(removedContexts, startKey);
- // Remove all remaining references to any removed contexts from the
- // hierarchy map.
- for (MergedContextConfiguration currentKey : removedContexts) {
- for (Set children : hierarchyMap.values()) {
- children.remove(currentKey);
- }
+ // Remove all remaining references to any removed contexts from the
+ // hierarchy map.
+ for (MergedContextConfiguration currentKey : removedContexts) {
+ for (Set children : this.hierarchyMap.values()) {
+ children.remove(currentKey);
}
+ }
- // Remove empty entries from the hierarchy map.
- for (MergedContextConfiguration currentKey : hierarchyMap.keySet()) {
- if (hierarchyMap.get(currentKey).isEmpty()) {
- hierarchyMap.remove(currentKey);
- }
+ // Remove empty entries from the hierarchy map.
+ for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) {
+ if (this.hierarchyMap.get(currentKey).isEmpty()) {
+ this.hierarchyMap.remove(currentKey);
}
}
}
@@ -239,26 +203,23 @@ class ContextCache {
private void remove(List removedContexts, MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
- synchronized (monitor) {
- Set children = hierarchyMap.get(key);
- if (children != null) {
- for (MergedContextConfiguration child : children) {
- // Recurse through lower levels
- remove(removedContexts, child);
- }
- // Remove the set of children for the current context from the
- // hierarchy map.
- hierarchyMap.remove(key);
+ Set 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 = contextMap.remove(key);
- if (context instanceof ConfigurableApplicationContext) {
- ((ConfigurableApplicationContext) context).close();
- }
- removedContexts.add(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);
}
/**
@@ -266,34 +227,30 @@ class ContextCache {
* contains more than Integer.MAX_VALUE elements, returns
* Integer.MAX_VALUE.
*/
- int size() {
- synchronized (monitor) {
- return this.contextMap.size();
- }
+ public int size() {
+ return this.contextMap.size();
}
/**
* Determine the number of parent contexts currently tracked within the cache.
*/
- int getParentContextCount() {
- synchronized (monitor) {
- return this.hierarchyMap.size();
- }
+ public int getParentContextCount() {
+ return this.hierarchyMap.size();
}
/**
* Generates a text string, which contains the {@linkplain #size() size} as well
- * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss}, and
- * {@linkplain #getParentContextCount() parent context} counts.
+ * as the {@linkplain #getHitCount() hit}, {@linkplain #getMissCount() miss},
+ * and {@linkplain #getParentContextCount() parent context} counts.
*/
@Override
public String toString() {
- return new ToStringCreator(this)//
- .append("size", size())//
- .append("hitCount", getHitCount())//
- .append("missCount", getMissCount())//
- .append("parentContextCount", getParentContextCount())//
- .toString();
+ return new ToStringCreator(this)
+ .append("size", size())
+ .append("hitCount", getHitCount())
+ .append("missCount", getMissCount())
+ .append("parentContextCount", getParentContextCount())
+ .toString();
}
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java
index 22d6580a3df..0c14c772293 100644
--- a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java
+++ b/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2014 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.
@@ -78,8 +78,7 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext
* @param testClass the test class for which the test context should be
* constructed (must not be {@code null})
* @param contextCache the context cache from which the constructed test
- * context should retrieve application contexts (must not be
- * {@code null})
+ * context should retrieve application contexts (must not be {@code null})
* @param defaultContextLoaderClassName the name of the default
* {@code ContextLoader} class to use (may be {@code null})
*/
@@ -90,73 +89,56 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext
this.testClass = testClass;
this.contextCache = contextCache;
this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache);
- this.mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
- defaultContextLoaderClassName, cacheAwareContextLoaderDelegate);
+ this.mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(
+ testClass, defaultContextLoaderClassName, this.cacheAwareContextLoaderDelegate);
}
- /**
- * {@inheritDoc}
- */
+
public ApplicationContext getApplicationContext() {
- return cacheAwareContextLoaderDelegate.loadContext(mergedContextConfiguration);
+ return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
}
- /**
- * {@inheritDoc}
- */
public final Class> getTestClass() {
- return testClass;
+ return this.testClass;
}
- /**
- * {@inheritDoc}
- */
public final Object getTestInstance() {
- return testInstance;
+ return this.testInstance;
}
- /**
- * {@inheritDoc}
- */
public final Method getTestMethod() {
- return testMethod;
+ return this.testMethod;
}
- /**
- * {@inheritDoc}
- */
public final Throwable getTestException() {
- return testException;
+ return this.testException;
}
- /**
- * {@inheritDoc}
- */
public void markApplicationContextDirty(HierarchyMode hierarchyMode) {
- contextCache.remove(mergedContextConfiguration, hierarchyMode);
+ synchronized (this.contextCache) {
+ this.contextCache.remove(this.mergedContextConfiguration, hierarchyMode);
+ }
}
- /**
- * {@inheritDoc}
- */
public void updateState(Object testInstance, Method testMethod, Throwable testException) {
this.testInstance = testInstance;
this.testMethod = testMethod;
this.testException = testException;
}
+
/**
* Provide a String representation of this test context's state.
*/
@Override
public String toString() {
- return new ToStringCreator(this)//
- .append("testClass", testClass)//
- .append("testInstance", testInstance)//
- .append("testMethod", testMethod)//
- .append("testException", testException)//
- .append("mergedContextConfiguration", mergedContextConfiguration)//
- .toString();
+ return new ToStringCreator(this)
+ .append("testClass", this.testClass)
+ .append("testInstance", this.testInstance)
+ .append("testMethod", this.testMethod)
+ .append("testException", this.testException)
+ .append("mergedContextConfiguration", this.mergedContextConfiguration)
+ .toString();
}
}
diff --git a/spring-test/src/test/resources/log4j.properties b/spring-test/src/test/resources/log4j.properties
index 5947420f135..41d968442b8 100644
--- a/spring-test/src/test/resources/log4j.properties
+++ b/spring-test/src/test/resources/log4j.properties
@@ -15,6 +15,7 @@ log4j.logger.org.springframework.test.context.ContextLoaderUtils=WARN
log4j.logger.org.springframework.test.context.transaction.TransactionalTestExecutionListener=WARN
log4j.logger.org.springframework.test.context.web=WARN
log4j.logger.org.springframework.test.context=WARN
+log4j.logger.org.springframework.test.context.cache=WARN
#log4j.logger.org.springframework.test.context.support.DelegatingSmartContextLoader=INFO
#log4j.logger.org.springframework.test.context.support.AbstractGenericContextLoader=INFO
diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc
index cb795d28b5e..c23941ec762 100644
--- a/src/asciidoc/index.adoc
+++ b/src/asciidoc/index.adoc
@@ -19503,6 +19503,12 @@ framework will not be able to cache application contexts between test classes an
build process will run significantly slower as a result.
====
+Since having a large number of application contexts loaded within a given test suite can
+cause the suite to take an unnecessarily long time to execute, it is often beneficial to
+know exactly how many contexts have been loaded and cached. To view the statistics for
+the underlying context cache, simply set the log level for the
+`org.springframework.test.context.cache` logging category to `DEBUG`.
+
In the unlikely case that a test corrupts the application context and requires reloading
-- for example, by modifying a bean definition or the state of an application object --
you can annotate your test class or test method with `@DirtiesContext` (see the
@@ -19512,6 +19518,7 @@ context before executing the next test. Note that support for the `@DirtiesConte
annotation is provided by the `DirtiesContextTestExecutionListener` which is enabled by
default.
+
[[testcontext-ctx-management-ctx-hierarchies]]
====== Context hierarchies