Skip to content

Commit

Permalink
[SPR-8549] ContextCache is now keyed by MergedContextConfiguration in…
Browse files Browse the repository at this point in the history
…stead of String; MergedContextConfiguration now implements custom hashCode() and equals() methods and no longer generates a context cache key.
  • Loading branch information
sbrannen committed Jul 20, 2011
1 parent 2d7af57 commit 8224af1
Show file tree
Hide file tree
Showing 4 changed files with 369 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2011 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.
Expand All @@ -16,7 +16,6 @@

package org.springframework.test.context;

import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

Expand All @@ -29,8 +28,8 @@
* Cache for Spring {@link ApplicationContext ApplicationContexts}
* in a test environment.
*
* <p>Maintains a cache of {@link ApplicationContext contexts} by
* {@link Serializable serializable} key. This has significant performance
* <p>Maintains a cache of {@link ApplicationContext contexts} keyed by
* {@link MergedContextConfiguration} instances. This has significant performance
* benefits if initializing the context would take time. While initializing a
* Spring context itself is very quick, some beans in a context, such as a
* {@link org.springframework.orm.hibernate3.LocalSessionFactoryBean LocalSessionFactoryBean}
Expand All @@ -46,7 +45,7 @@ class ContextCache {
/**
* Map of context keys to Spring ApplicationContext instances.
*/
private final Map<String, ApplicationContext> contextKeyToContextMap = new ConcurrentHashMap<String, ApplicationContext>();
private final Map<MergedContextConfiguration, ApplicationContext> contextMap = new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>();

private int hitCount;

Expand All @@ -57,7 +56,7 @@ class ContextCache {
* Clears all contexts from the cache.
*/
void clear() {
this.contextKeyToContextMap.clear();
this.contextMap.clear();
}

/**
Expand All @@ -73,9 +72,9 @@ void clearStatistics() {
* Return whether there is a cached context for the given key.
* @param key the context key (never <code>null</code>)
*/
boolean contains(String key) {
boolean contains(MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
return this.contextKeyToContextMap.containsKey(key);
return this.contextMap.containsKey(key);
}

/**
Expand All @@ -87,9 +86,9 @@ boolean contains(String key) {
* or <code>null</code> if not found in the cache.
* @see #remove
*/
ApplicationContext get(String key) {
ApplicationContext get(MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
ApplicationContext context = this.contextKeyToContextMap.get(key);
ApplicationContext context = this.contextMap.get(key);
if (context == null) {
incrementMissCount();
}
Expand Down Expand Up @@ -137,10 +136,10 @@ int getMissCount() {
* @param key the context key (never <code>null</code>)
* @param context the ApplicationContext instance (never <code>null</code>)
*/
void put(String 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.contextKeyToContextMap.put(key, context);
this.contextMap.put(key, context);
}

/**
Expand All @@ -150,8 +149,8 @@ void put(String key, ApplicationContext context) {
* or <code>null</code> if not found in the cache.
* @see #setDirty
*/
ApplicationContext remove(String key) {
return this.contextKeyToContextMap.remove(key);
ApplicationContext remove(MergedContextConfiguration key) {
return this.contextMap.remove(key);
}

/**
Expand All @@ -165,7 +164,7 @@ ApplicationContext remove(String key) {
* @param key the context key (never <code>null</code>)
* @see #remove
*/
void setDirty(String key) {
void setDirty(MergedContextConfiguration key) {
Assert.notNull(key, "Key must not be null");
ApplicationContext context = remove(key);
if (context instanceof ConfigurableApplicationContext) {
Expand All @@ -179,7 +178,7 @@ void setDirty(String key) {
* <tt>Integer.MAX_VALUE</tt>.
*/
int size() {
return this.contextKeyToContextMap.size();
return this.contextMap.size();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.test.context;

import java.io.Serializable;
import java.util.Arrays;
import java.util.SortedSet;
import java.util.TreeSet;
Expand All @@ -25,7 +26,7 @@
import org.springframework.util.StringUtils;

/**
* <code>MergedContextConfiguration</code> encapsulates the <em>merged</em>
* {@code MergedContextConfiguration} encapsulates the <em>merged</em>
* context configuration declared on a test class and all of its superclasses
* via {@link ContextConfiguration @ContextConfiguration} and
* {@link ActiveProfiles @ActiveProfiles}.
Expand All @@ -37,33 +38,34 @@
* {@link ActiveProfiles#inheritProfiles inheritProfiles} flags in
* {@code @ContextConfiguration} and {@code @ActiveProfiles}, respectively.
*
* <p>A {@link SmartContextLoader} uses <code>MergedContextConfiguration</code>
* <p>A {@link SmartContextLoader} uses {@code MergedContextConfiguration}
* to load an {@link org.springframework.context.ApplicationContext ApplicationContext}.
*
* <p>{@code MergedContextConfiguration} is also used by the {@link TestContext}
* as the context cache key for caching an
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* that was loaded using properties of this {@code MergedContextConfiguration}.
*
* @author Sam Brannen
* @since 3.1
* @see ContextConfiguration
* @see ActiveProfiles
* @see ContextConfigurationAttributes
* @see SmartContextLoader#loadContext(MergedContextConfiguration)
*/
public class MergedContextConfiguration {
public class MergedContextConfiguration implements Serializable {

private static final long serialVersionUID = -3290560718464957422L;

private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];

private final Class<?> testClass;

private final String[] locations;

private final Class<?>[] classes;

private final String[] activeProfiles;

private final ContextLoader contextLoader;

private final String contextKey;


private static String[] processLocations(String[] locations) {
return locations == null ? EMPTY_STRING_ARRAY : locations;
Expand All @@ -86,29 +88,16 @@ private static String[] processActiveProfiles(String[] activeProfiles) {
}

/**
* Generate a null-safe {@link String} representation of the supplied {@link ContextLoader}.
* Generate a null-safe {@link String} representation of the supplied
* {@link ContextLoader} based solely on the fully qualified name of the
* loader or &quot;null&quot; if the supplied loaded is <code>null</code>.
*/
private static String nullSafeToString(ContextLoader contextLoader) {
return contextLoader == null ? "null" : contextLoader.getClass().getName();
}

/**
* Generate a context <em>key</em> from the supplied values.
*/
private static String generateContextKey(String[] locations, Class<?>[] classes, String[] activeProfiles,
ContextLoader contextLoader) {

String locationsKey = ObjectUtils.nullSafeToString(locations);
String classesKey = ObjectUtils.nullSafeToString(classes);
String activeProfilesKey = ObjectUtils.nullSafeToString(activeProfiles);
String contextLoaderKey = nullSafeToString(contextLoader);

return String.format("locations = %s, classes = %s, activeProfiles = %s, contextLoader = %s", locationsKey,
classesKey, activeProfilesKey, contextLoaderKey);
}

/**
* Create a new <code>MergedContextConfiguration</code> instance for the
* Create a new {@code MergedContextConfiguration} instance for the
* supplied {@link Class test class}, resource locations, configuration
* classes, active profiles, and {@link ContextLoader}.
* <p>If a <code>null</code> value is supplied for <code>locations</code>,
Expand All @@ -128,12 +117,11 @@ public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<
this.classes = processClasses(classes);
this.activeProfiles = processActiveProfiles(activeProfiles);
this.contextLoader = contextLoader;
this.contextKey = generateContextKey(this.locations, this.classes, this.activeProfiles, this.contextLoader);
}

/**
* Get the {@link Class test class} associated with this
* <code>MergedContextConfiguration</code>.
* {@code MergedContextConfiguration}.
*/
public Class<?> getTestClass() {
return testClass;
Expand Down Expand Up @@ -172,20 +160,59 @@ public ContextLoader getContextLoader() {
}

/**
* Get the unique context key for all properties of this
* <code>MergedContextConfiguration</code> excluding the
* {@link #getTestClass() test class}.
* <p>Intended to be used for caching an
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* that was loaded using properties of this <code>MergedContextConfiguration</code>.
* Generate a unique hash code for all properties of this
* {@code MergedContextConfiguration} excluding the
* {@link #getTestClass() test class}.
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(locations);
result = prime * result + Arrays.hashCode(classes);
result = prime * result + Arrays.hashCode(activeProfiles);
result = prime * result + nullSafeToString(contextLoader).hashCode();
return result;
}

/**
* TODO Document equals() implementation.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public String getContextKey() {
return contextKey;
@Override
public boolean equals(Object obj) {

if (this == obj) {
return true;
}
if (!(obj instanceof MergedContextConfiguration)) {
return false;
}

final MergedContextConfiguration that = (MergedContextConfiguration) obj;

if (!Arrays.equals(this.locations, that.locations)) {
return false;
}
if (!Arrays.equals(this.classes, that.classes)) {
return false;
}
if (!Arrays.equals(this.activeProfiles, that.activeProfiles)) {
return false;
}
if (!nullSafeToString(this.contextLoader).equals(nullSafeToString(that.contextLoader))) {
return false;
}

return true;
}

/**
* Provide a String representation of the test class, merged context
* configuration, and context key.
* Provide a String representation of the {@link #getTestClass() test class},
* {@link #getLocations() locations}, {@link #getClasses() configuration classes},
* {@link #getActiveProfiles() active profiles}, and the name of the
* {@link #getContextLoader() ContextLoader}.
*/
@Override
public String toString() {
Expand All @@ -195,7 +222,6 @@ public String toString() {
.append("classes", ObjectUtils.nullSafeToString(classes))//
.append("activeProfiles", ObjectUtils.nullSafeToString(activeProfiles))//
.append("contextLoader", nullSafeToString(contextLoader))//
.append("contextKey", contextKey)//
.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class TestContext extends AttributeAccessorSupport {
this(testClass, contextCache, null);
}

// TODO Update regarding default --> DelegatingSmartContextLoader
/**
* Construct a new test context for the supplied {@link Class test class}
* and {@link ContextCache context cache} and parse the corresponding
Expand Down Expand Up @@ -141,18 +142,17 @@ private ApplicationContext loadApplicationContext() throws Exception {
* application context
*/
public ApplicationContext getApplicationContext() {
String contextKey = mergedContextConfiguration.getContextKey();
synchronized (contextCache) {
ApplicationContext context = contextCache.get(contextKey);
ApplicationContext context = contextCache.get(mergedContextConfiguration);
if (context == null) {
try {
context = loadApplicationContext();
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Storing ApplicationContext for test class [%s] in cache under key [%s].", testClass,
contextKey));
mergedContextConfiguration));
}
contextCache.put(contextKey, context);
contextCache.put(mergedContextConfiguration, context);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load ApplicationContext", ex);
Expand All @@ -162,7 +162,7 @@ public ApplicationContext getApplicationContext() {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Retrieved ApplicationContext for test class [%s] from cache with key [%s].", testClass,
contextKey));
mergedContextConfiguration));
}
}
return context;
Expand Down Expand Up @@ -217,7 +217,7 @@ public final Throwable getTestException() {
*/
public void markApplicationContextDirty() {
synchronized (contextCache) {
contextCache.setDirty(mergedContextConfiguration.getContextKey());
contextCache.setDirty(mergedContextConfiguration);
}
}

Expand Down
Loading

0 comments on commit 8224af1

Please sign in to comment.