Browse Source
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-12683pull/783/head
18 changed files with 666 additions and 331 deletions
@ -0,0 +1,270 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue