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 1a59940476..5dfd5705b2 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-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode; /** * A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain * #loadContext loading} and {@linkplain #closeContext closing} application - * contexts, interacting transparently with a context cache behind + * contexts, interacting transparently with a {@link ContextCache} behind * the scenes. * *

Note: {@code CacheAwareContextLoaderDelegate} does not extend the @@ -38,7 +38,7 @@ public interface CacheAwareContextLoaderDelegate { * Load the {@linkplain ApplicationContext application context} for the supplied * {@link MergedContextConfiguration} by delegating to the {@link ContextLoader} * configured in the given {@code MergedContextConfiguration}. - *

If the context is present in the context cache it will simply + *

If the context is present in the {@code ContextCache} it will simply * be returned; otherwise, it will be loaded, stored in the cache, and returned. * @param mergedContextConfiguration the merged context configuration to use * to load the application context; never {@code null} @@ -50,7 +50,7 @@ public interface CacheAwareContextLoaderDelegate { /** * Remove the {@linkplain ApplicationContext application context} for the - * supplied {@link MergedContextConfiguration} from the context cache + * supplied {@link MergedContextConfiguration} from the {@code ContextCache} * and {@linkplain ConfigurableApplicationContext#close() close} it if it is * an instance of {@link ConfigurableApplicationContext}. *

The semantics of the supplied {@code HierarchyMode} must be honored when 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 5f18399d80..6dafc4a4cd 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 @@ -16,145 +16,48 @@ package org.springframework.test.context; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.style.ToStringCreator; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; -import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; /** - * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test - * environment. + * {@code ContextCache} defines the public API for caching Spring + * {@link ApplicationContext ApplicationContexts} within the Spring + * TestContext Framework. + * + *

A {@code ContextCache} maintains a cache of {@code ApplicationContexts} + * keyed by {@link MergedContextConfiguration} instances. * *

Rationale

- *

Caching has significant performance benefits if initializing the context - * takes a considerable about of time. Although initializing a Spring context - * itself is very quick, some beans in a context, such as a - * {@code LocalSessionFactoryBean} for working with Hibernate, may take some - * time to initialize. Hence it often makes sense to perform that initialization - * only once per test suite. - * - *

Implementation Details

- *

{@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. + *

Context caching can have significant performance benefits if context + * initialization is complex. So, although initializing a Spring context itself + * is typically very quick, some beans in a context — for example, an + * in-memory database or a {@code LocalSessionFactoryBean} for working with + * Hibernate — may take several seconds to initialize. Hence it often + * makes sense to perform that initialization only once per test suite. * * @author Sam Brannen * @author Juergen Hoeller - * @since 2.5 - * @see ConcurrentReferenceHashMap + * @since 4.2 */ -class ContextCache { - - /** - * Map of context keys to Spring {@code ApplicationContext} instances. - */ - private final Map contextMap = - new ConcurrentReferenceHashMap(64); - - /** - * Map of parent keys to sets of children keys, representing a top-down tree - * 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> hierarchyMap = - new ConcurrentReferenceHashMap>(64); - - private final AtomicInteger hitCount = new AtomicInteger(); - - private final AtomicInteger missCount = new AtomicInteger(); - - /** - * Reset all state maintained by this cache. - * @see #clear() - * @see #clearStatistics() - */ - public void reset() { - synchronized (contextMap) { - clear(); - clearStatistics(); - } - } - - /** - * Clear all contexts from the cache and clear context hierarchy information as well. - */ - public void clear() { - synchronized (contextMap) { - this.contextMap.clear(); - this.hierarchyMap.clear(); - } - } - - /** - * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). - */ - public void clearStatistics() { - synchronized (contextMap) { - this.hitCount.set(0); - this.missCount.set(0); - } - } +public interface ContextCache { /** * Determine whether there is a cached context for the given key. * @param key the context key (never {@code null}) * @return {@code true} if the cache contains a context with the given key */ - public boolean contains(MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - return this.contextMap.containsKey(key); - } + boolean contains(MergedContextConfiguration key); /** * Obtain a cached {@code ApplicationContext} for the given key. - *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will - * be updated accordingly. + *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts + * must be updated accordingly. * @param key the context key (never {@code null}) * @return the corresponding {@code ApplicationContext} instance, or {@code null} * if not found in the cache * @see #remove */ - public ApplicationContext get(MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - ApplicationContext context = this.contextMap.get(key); - if (context == null) { - this.missCount.incrementAndGet(); - } - else { - this.hitCount.incrementAndGet(); - } - return context; - } - - /** - * Get the overall hit count for this cache. - *

A hit 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. - *

A miss is any access to the cache that returns a {@code null} - * context for the queried key. - */ - public int getMissCount() { - return this.missCount.get(); - } + ApplicationContext get(MergedContextConfiguration key); /** * Explicitly add an {@code ApplicationContext} instance to the cache @@ -162,122 +65,80 @@ class ContextCache { * @param key the context key (never {@code null}) * @param context the {@code ApplicationContext} instance (never {@code null}) */ - public void put(MergedContextConfiguration key, ApplicationContext context) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(context, "ApplicationContext must not be null"); - - this.contextMap.put(key, context); - MergedContextConfiguration child = key; - MergedContextConfiguration parent = child.getParent(); - while (parent != null) { - Set list = this.hierarchyMap.get(parent); - if (list == null) { - list = new HashSet(); - this.hierarchyMap.put(parent, list); - } - list.add(child); - child = parent; - parent = child.getParent(); - } - } + void put(MergedContextConfiguration key, ApplicationContext context); /** * Remove the context with the given key from the cache and explicitly - * {@linkplain ConfigurableApplicationContext#close() close} it if it is an - * instance of {@link ConfigurableApplicationContext}. - *

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 + * {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close} + * it if it is an instance of {@code ConfigurableApplicationContext}. + *

Generally speaking, this method should be called if the state of + * a singleton bean has been modified, potentially affecting future + * interaction with the context. + *

In addition, the semantics of the supplied {@code HierarchyMode} must * be honored. See the Javadoc for {@link HierarchyMode} for details. * @param key the context key; never {@code null} * @param hierarchyMode the hierarchy mode; may be {@code null} if the context * is not part of a hierarchy */ - public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { - Assert.notNull(key, "Key must not be null"); - - // startKey is the level at which to begin clearing the cache, depending - // on the configured hierarchy mode. - MergedContextConfiguration startKey = key; - if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { - while (startKey.getParent() != null) { - startKey = startKey.getParent(); - } - } - - List 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 : 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 removedContexts, MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - - 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 = this.contextMap.remove(key); - if (context instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) context).close(); - } - removedContexts.add(key); - } + void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode); /** * Determine the number of contexts currently stored in the cache. *

If the cache contains more than {@code Integer.MAX_VALUE} elements, - * this method returns {@code Integer.MAX_VALUE}. + * this method must return {@code Integer.MAX_VALUE}. */ - public int size() { - return this.contextMap.size(); - } + int size(); /** * Determine the number of parent contexts currently tracked within the cache. */ - public int getParentContextCount() { - return this.hierarchyMap.size(); - } + int getParentContextCount(); /** - * Generate a text string containing the statistics for this cache. - *

Specifically, the returned string contains the {@linkplain #size}, - * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() miss count}, - * and {@linkplain #getParentContextCount() parent context count}. - * @return the statistics for this cache + * Get the overall hit count for this cache. + *

A hit is any access to the cache that returns a non-null + * context for the queried key. + */ + int getHitCount(); + + /** + * Get the overall miss count for this cache. + *

A miss is any access to the cache that returns a {@code null} + * context for the queried key. + */ + int getMissCount(); + + /** + * Reset all state maintained by this cache including statistics. + * @see #clear() + * @see #clearStatistics() + */ + void reset(); + + /** + * Clear all contexts from the cache, clearing context hierarchy information as well. + */ + void clear(); + + /** + * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). + */ + void clearStatistics(); + + /** + * Generate a text string containing the implementation type of this + * cache and its statistics. + *

The value returned by this method will be used primarily for + * logging purposes. + *

Specifically, the returned string should contain the name of the + * concrete {@code ContextCache} implementation, the {@linkplain #size}, + * {@linkplain #getParentContextCount() parent context count}, + * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() + * miss count}, and any other information useful in monitoring the + * state of this cache. + * @return a string representation of this cache, including statistics */ @Override - public String toString() { - return new ToStringCreator(this) - .append("size", size()) - .append("hitCount", getHitCount()) - .append("missCount", getMissCount()) - .append("parentContextCount", getParentContextCount()) - .toString(); - } + abstract String toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java index a13e347c2c..cfa482c321 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java @@ -21,16 +21,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.support.DefaultContextCache; import org.springframework.util.Assert; /** * Default implementation of the {@link CacheAwareContextLoaderDelegate} interface. * - *

Although {@code DefaultCacheAwareContextLoaderDelegate} was first introduced - * in Spring Framework 4.1, the initial implementation of this class was extracted - * from the existing code base for {@code CacheAwareContextLoaderDelegate} when - * {@code CacheAwareContextLoaderDelegate} was converted into an interface. - * * @author Sam Brannen * @since 4.1 */ @@ -47,21 +43,21 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD * and reused for all subsequent tests that declare the same unique * context configuration within the same JVM process. */ - static final ContextCache defaultContextCache = new ContextCache(); + static final ContextCache defaultContextCache = new DefaultContextCache(); private final ContextCache contextCache; /** - * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} that - * uses the default, static {@code ContextCache}. + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using + * a static {@code DefaultContextCache}. */ DefaultCacheAwareContextLoaderDelegate() { this(defaultContextCache); } /** - * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} with + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using * the supplied {@code ContextCache}. */ DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java new file mode 100644 index 0000000000..17ebcb82b1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java @@ -0,0 +1,252 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.support; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.ContextCache; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Default implementation of the {@link ContextCache} API. + * + *

Uses Spring's {@link ConcurrentReferenceHashMap} to store + * {@linkplain java.lang.ref.SoftReference soft references} to cached + * contexts and {@code MergedContextConfiguration} instances. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ConcurrentReferenceHashMap + */ +public class DefaultContextCache implements ContextCache { + + /** + * Map of context keys to Spring {@code ApplicationContext} instances. + */ + private final Map contextMap = + new ConcurrentReferenceHashMap(64); + + /** + * Map of parent keys to sets of children keys, representing a top-down tree + * 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> hierarchyMap = + new ConcurrentReferenceHashMap>(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 list = this.hierarchyMap.get(parent); + if (list == null) { + list = new HashSet(); + 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 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 : 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 removedContexts, MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + + 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 = this.contextMap.remove(key); + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + removedContexts.add(key); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return this.contextMap.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getParentContextCount() { + return this.hierarchyMap.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getHitCount() { + return this.hitCount.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getMissCount() { + return this.missCount.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void reset() { + synchronized (contextMap) { + clear(); + clearStatistics(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + synchronized (contextMap) { + this.contextMap.clear(); + this.hierarchyMap.clear(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearStatistics() { + synchronized (contextMap) { + this.hitCount.set(0); + this.missCount.set(0); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("size", size()) + .append("parentContextCount", getParentContextCount()) + .append("hitCount", getHitCount()) + .append("missCount", getMissCount()) + .toString(); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java index 61dc9beaa3..abd36808f8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java @@ -18,11 +18,11 @@ package org.springframework.test.context; import org.junit.Before; import org.junit.Test; - import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.DefaultContextCache; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.*; @@ -39,7 +39,7 @@ import static org.springframework.test.context.ContextCacheTestUtils.*; */ public class ContextCacheTests { - private ContextCache contextCache = new ContextCache(); + private ContextCache contextCache = new DefaultContextCache(); @Before