[SPR-8386][SPR-8387] Redesign of DelegatingSmartContextLoader and the SmartContextLoader SPI:

- Removed generatesDefaults() and supports() from the SmartContextLoader SPI and modified the DelegatingSmartContextLoader algorithm accordingly.
- DelegatingSmartContextLoader no longer operates on a list of candidate loaders but rather on explicit instances of GenericXmlContextLoader and AnnotationConfigContextLoader.
- Updated Javadoc regarding DelegatingSmartContextLoader as the new default loader.
- Updated and polished Javadoc regarding changes in 3.1.
- Now enforcing @ContextConfiguration's restriction on declaring locations or classes but not both.
This commit is contained in:
Sam Brannen
2011-08-13 14:31:00 +00:00
parent 2d6340af74
commit a298c2dde8
13 changed files with 336 additions and 251 deletions

View File

@@ -103,8 +103,8 @@ public @interface ContextConfiguration {
* for details on the default configuration classes that will be used if none
* are specified.
*
* <p>Note that this attribute may <strong>not</strong> be used in
* conjunction with {@link #locations} or {@link #value}.
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #locations} or {@link #value}.
* @since 3.1
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.test.context.support.AnnotationConfigContextLoader
@@ -158,12 +158,12 @@ public @interface ContextConfiguration {
* {@code ExtendedConfig} may therefore override those defined in
* {@code BaseConfig}.
* <pre class="code">
* &#064;ContextConfiguration(classes=BaseConfig.class, loader=AnnotationConfigContextLoader.class)
* &#064;ContextConfiguration(classes=BaseConfig.class)
* public class BaseTest {
* // ...
* }
*
* &#064;ContextConfiguration(classes=ExtendedConfig.class, loader=AnnotationConfigContextLoader.class)
* &#064;ContextConfiguration(classes=ExtendedConfig.class)
* public class ExtendedTest extends BaseTest {
* // ...
* }
@@ -172,7 +172,6 @@ public @interface ContextConfiguration {
*/
boolean inheritLocations() default true;
// TODO Update regarding default --> DelegatingSmartContextLoader
/**
* The type of {@link ContextLoader} (or {@link SmartContextLoader}) to use
* for loading an {@link org.springframework.context.ApplicationContext
@@ -184,10 +183,15 @@ public @interface ContextConfiguration {
* loader, a default loader will be used instead.
*
* <p>The default concrete implementation chosen at runtime will be
* {@link org.springframework.test.context.support.GenericXmlContextLoader
* GenericXmlContextLoader}. Also check out
* {@link org.springframework.test.context.support.DelegatingSmartContextLoader
* DelegatingSmartContextLoader}. For further details on the default behavior
* of various concrete {@code ContextLoaders}, check out the Javadoc for
* {@link org.springframework.test.context.support.AbstractContextLoader
* AbstractContextLoader}'s javadoc for details on the default behavior there.
* AbstractContextLoader},
* {@link org.springframework.test.context.support.GenericXmlContextLoader
* GenericXmlContextLoader}, and
* {@link org.springframework.test.context.support.AnnotationConfigContextLoader
* AnnotationConfigContextLoader}.
* @since 2.5
*/
Class<? extends ContextLoader> loader() default ContextLoader.class;

View File

@@ -99,6 +99,8 @@ public class ContextConfigurationAttributes {
* @param classes the configuration classes declared via {@code @ContextConfiguration}
* @param inheritLocations the <code>inheritLocations</code> flag declared via {@code @ContextConfiguration}
* @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration}
* @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is
* <code>null</code>, or if the {@code locations} and {@code classes} are both non-empty
*/
public ContextConfigurationAttributes(Class<?> declaringClass, String[] locations, Class<?>[] classes,
boolean inheritLocations, Class<? extends ContextLoader> contextLoaderClass) {
@@ -106,6 +108,16 @@ public class ContextConfigurationAttributes {
Assert.notNull(declaringClass, "declaringClass must not be null");
Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null");
if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes)) {
String msg = String.format(
"Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s "
+ "and 'classes' %s attributes. Only one declaration of resources "
+ "is permitted per @ContextConfiguration annotation.", declaringClass.getName(),
ObjectUtils.nullSafeToString(locations), ObjectUtils.nullSafeToString(classes));
logger.error(msg);
throw new IllegalArgumentException(msg);
}
this.declaringClass = declaringClass;
this.locations = locations;
this.classes = classes;
@@ -169,16 +181,38 @@ public class ContextConfigurationAttributes {
this.classes = classes;
}
/**
* Determine if this {@code ContextConfigurationAttributes} instance has
* path-based resource locations.
* @return <code>true</code> if the {@link #getLocations() locations} array is not empty
* @see #hasResources()
* @see #hasClasses()
*/
public boolean hasLocations() {
return !ObjectUtils.isEmpty(getLocations());
}
/**
* Determine if this {@code ContextConfigurationAttributes} instance has
* class-based resources.
* @return <code>true</code> if the {@link #getClasses() classes} array is not empty
* @see #hasResources()
* @see #hasLocations()
*/
public boolean hasClasses() {
return !ObjectUtils.isEmpty(getClasses());
}
/**
* Determine if this {@code ContextConfigurationAttributes} instance has
* either path-based resource locations or class-based resources.
* @return <code>true</code> if either the {@link #getLocations() locations}
* or the {@link #getClasses() classes} array is not empty
* @see #getLocations()
* @see #getClasses()
* @see #hasLocations()
* @see #hasClasses()
*/
public boolean hasResources() {
return !ObjectUtils.isEmpty(getLocations()) || !ObjectUtils.isEmpty(getClasses());
return hasLocations() || hasClasses();
}
/**

View File

@@ -22,34 +22,37 @@ import org.springframework.context.ApplicationContext;
* <p>Strategy interface for loading an {@link ApplicationContext application context}
* for an integration test managed by the Spring TestContext Framework.
*
* <p>The <code>SmartContextLoader</code> SPI supersedes the {@link ContextLoader}
* SPI introduced in Spring 2.5: <code>SmartContextLoaders</code> can process both
* resource locations and configuration classes. Furthermore, a
* <code>SmartContextLoader</code> can set active bean definition profiles
* in the context that it loads (see {@link MergedContextConfiguration#getActiveProfiles()}
* and {@link #loadContext(MergedContextConfiguration)}).
* <p>The {@code SmartContextLoader} SPI supersedes the {@link ContextLoader}
* SPI introduced in Spring 2.5: a {@code SmartContextLoader} can process both
* resource locations and configuration classes. Furthermore, a {@code SmartContextLoader}
* can set active bean definition profiles in the context that it loads (see
* {@link MergedContextConfiguration#getActiveProfiles()} and
* {@link #loadContext(MergedContextConfiguration)}).
*
* <p>Clients of a <code>SmartContextLoader</code> should call
* {@link #processContextConfiguration(ContextConfigurationAttributes) processContextConfiguration()}
* prior to calling {@link #loadContext(MergedContextConfiguration) loadContext()}
* in case the <code>SmartContextLoader</code> provides custom support for modifying
* or generating resource locations or configuration classes. The results of
* {@link #processContextConfiguration(ContextConfigurationAttributes) processContextConfiguration()}
* should be merged for all classes in the hierarchy of the root test class and
* then supplied to {@link #loadContext(MergedContextConfiguration) loadContext()}.
* <p>Clients of a {@code SmartContextLoader} should call
* {@link #processContextConfiguration(ContextConfigurationAttributes)
* processContextConfiguration()} prior to calling
* {@link #loadContext(MergedContextConfiguration) loadContext()}. This gives a
* {@code SmartContextLoader} the opportunity to provide custom support for
* modifying or generating resource locations or configuration classes. The
* results of {@link #processContextConfiguration(ContextConfigurationAttributes)
* processContextConfiguration()} should be merged for all classes in the
* hierarchy of the root test class and then supplied to
* {@link #loadContext(MergedContextConfiguration) loadContext()}.
*
* <p>Even though <code>SmartContextLoader</code> extends <code>ContextLoader</code>,
* clients should favor <code>SmartContextLoader</code>-specific methods over those
* defined in <code>ContextLoader</code>, particularly because a
* <code>SmartContextLoader</code> may choose not to support methods defined in
* the <code>ContextLoader</code> SPI.
* <p>Even though {@code SmartContextLoader} extends {@code ContextLoader},
* clients should favor {@code SmartContextLoader}-specific methods over those
* defined in {@code ContextLoader}, particularly because a
* {@code SmartContextLoader} may choose not to support methods defined in
* the {@code ContextLoader} SPI.
*
* <p>Concrete implementations must provide a <code>public</code> no-args constructor.
*
* <p>Spring provides the following out-of-the-box implementations:
* <ul>
* <li>{@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}</li>
* <li>{@link org.springframework.test.context.support.DelegatingSmartContextLoader DelegatingSmartContextLoader}</li>
* <li>{@link org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader}</li>
* <li>{@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader}</li>
* <li>{@link org.springframework.test.context.support.GenericPropertiesContextLoader GenericPropertiesContextLoader}</li>
* </ul>
*
@@ -62,47 +65,25 @@ import org.springframework.context.ApplicationContext;
*/
public interface SmartContextLoader extends ContextLoader {
/**
* Determines if this <code>SmartContextLoader</code> generates default resource
* locations or {@link org.springframework.context.annotation.Configuration
* configuration classes} if the <code>locations</code> or <code>classes</code>
* present in the {@link ContextConfigurationAttributes} provided to
* {@link #processContextConfiguration()} are <code>null</code> or empty.
* <p>Returning a value of <code>true</code> signals not only that this
* <code>SmartContextLoader</code> generates defaults but also that it will
* <em>preemptively</em> verify that a generated default actually exists.
* @return <code>true</code> if this <code>SmartContextLoader</code>
* generates default configuration locations or classes
* @see #processContextConfiguration(ContextConfigurationAttributes)
*/
boolean generatesDefaults();
/**
* Processes the {@link ContextConfigurationAttributes} for a given test class.
* <p>Concrete implementations may choose to <em>modify</em> the <code>locations</code>
* or <code>classes</code> in the supplied {@link ContextConfigurationAttributes}
* or <em>generate default</em> configuration locations or classes if the
* supplied values are <code>null</code> or empty.
* <p><b>Note</b>: If {@link #generatesDefaults()} returns <code>true</code>,
* this method <b>must</b> <em>preemptively</em> verify that a generated default
* actually exists before setting the corresponding <code>locations</code>
* or <code>classes</code> property in the supplied
* or <code>classes</code> in the supplied {@link ContextConfigurationAttributes},
* <em>generate</em> default configuration locations, or <em>detect</em>
* default configuration classes if the supplied values are <code>null</code>
* or empty.
* <p><b>Note</b>: in contrast to a standard {@code ContextLoader}, a
* {@code SmartContextLoader} <b>must</b> <em>preemptively</em> verify that
* a generated or detected default actually exists before setting the corresponding
* <code>locations</code> or <code>classes</code> property in the supplied
* {@link ContextConfigurationAttributes}. Consequently, leaving the
* <code>locations</code> or <code>classes</code> property empty signals that
* this <code>SmartContextLoader</code> was not able to generate defaults.
* this {@code SmartContextLoader} was not able to generate or detect defaults.
* @param configAttributes the context configuration attributes to process
* @see #generatesDefaults()
*/
void processContextConfiguration(ContextConfigurationAttributes configAttributes);
/**
* TODO Document supports(MergedContextConfiguration).
*
* @param mergedConfig
* @return
*/
boolean supports(MergedContextConfiguration mergedConfig);
/**
* Loads a new {@link ApplicationContext context} based on the supplied
* {@link MergedContextConfiguration merged context configuration},
@@ -111,7 +92,7 @@ public interface SmartContextLoader extends ContextLoader {
* <p>Concrete implementations should register annotation configuration
* processors with bean factories of
* {@link ApplicationContext application contexts} loaded by this
* <code>SmartContextLoader</code>. Beans will therefore automatically be
* {@code SmartContextLoader}. Beans will therefore automatically be
* candidates for annotation-based dependency injection using
* {@link org.springframework.beans.factory.annotation.Autowired @Autowired},
* {@link javax.annotation.Resource @Resource}, and
@@ -119,10 +100,10 @@ public interface SmartContextLoader extends ContextLoader {
* should set the active bean definition profiles in the context's
* {@link org.springframework.core.env.Environment Environment}.
* <p>Any <code>ApplicationContext</code> loaded by a
* <code>SmartContextLoader</code> <strong>must</strong> register a JVM
* {@code SmartContextLoader} <strong>must</strong> register a JVM
* shutdown hook for itself. Unless the context gets closed early, all context
* instances will be automatically closed on JVM shutdown. This allows for
* freeing external resources held by beans within the context (e.g.,
* freeing of external resources held by beans within the context (e.g.,
* temporary files).
* @param mergedConfig the merged context configuration to use to load the
* application context

View File

@@ -54,32 +54,30 @@ public class TestContext extends AttributeAccessorSupport {
/**
* Delegates to {@link #TestContext(Class, ContextCache, String)} with a
* value of <code>null</code> for the default <code>ContextLoader</code>
* class name.
* value of <code>null</code> for the default {@code ContextLoader} class name.
*/
TestContext(Class<?> testClass, ContextCache contextCache) {
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
* {@link ContextConfiguration &#064;ContextConfiguration} annotation, if
* present.
* <p>If the supplied class name for the default <code>ContextLoader</code>
* is <code>null</code> or <em>empty</em> and no <code>ContextLoader</code>
* class is explicitly supplied via the
* <code>&#064;ContextConfiguration</code> annotation, a
* {@link org.springframework.test.context.support.GenericXmlContextLoader
* GenericXmlContextLoader} will be used instead.
* <p>If the supplied class name for the default {@code ContextLoader}
* is <code>null</code> or <em>empty</em> and no concrete {@code ContextLoader}
* class is explicitly supplied via the {@code @ContextConfiguration}
* annotation, a
* {@link org.springframework.test.context.support.DelegatingSmartContextLoader
* DelegatingSmartContextLoader} will be used instead.
* @param testClass the test class for which the test context should be
* constructed (must not be <code>null</code>)
* @param contextCache the context cache from which the constructed test
* context should retrieve application contexts (must not be
* <code>null</code>)
* @param defaultContextLoaderClassName the name of the default
* <code>ContextLoader</code> class to use (may be <code>null</code>)
* {@code ContextLoader} class to use (may be <code>null</code>)
*/
TestContext(Class<?> testClass, ContextCache contextCache, String defaultContextLoaderClassName) {
Assert.notNull(testClass, "Test class must not be null");
@@ -110,7 +108,7 @@ public class TestContext extends AttributeAccessorSupport {
/**
* Load an <code>ApplicationContext</code> for this test context using the
* configured <code>ContextLoader</code> and configuration attributes.
* configured {@code ContextLoader} and configuration attributes.
* @throws Exception if an error occurs while loading the application context
*/
private ApplicationContext loadApplicationContext() throws Exception {

View File

@@ -23,7 +23,6 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.test.context.ContextConfigurationAttributes;
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.ClassUtils;
@@ -40,9 +39,7 @@ import org.springframework.util.StringUtils;
* <p>As of Spring 3.1, <code>AbstractContextLoader</code> also provides a basis
* for all concrete implementations of the {@link SmartContextLoader} SPI. For
* backwards compatibility with the {@code ContextLoader} SPI,
* {@link #processContextConfiguration()} delegates to
* {@link #processLocations()}, and {@link #generatesDefaults()} delegates to
* {@link #isGenerateDefaultLocations()}.
* {@link #processContextConfiguration()} delegates to {@link #processLocations()}.
*
* @author Sam Brannen
* @author Juergen Hoeller
@@ -54,23 +51,12 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
private static final Log logger = LogFactory.getLog(AbstractContextLoader.class);
private static final String[] EMPTY_STRING_ARRAY = new String[] {};
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final String SLASH = "/";
// --- SmartContextLoader -----------------------------------------------
/**
* For backwards compatibility with the {@link ContextLoader} SPI, the
* default implementation simply delegates to
* {@link #isGenerateDefaultLocations()}.
* @see org.springframework.test.context.SmartContextLoader#generatesDefaults()
* @see #isGenerateDefaultLocations()
*/
public boolean generatesDefaults() {
return isGenerateDefaultLocations();
}
/**
* For backwards compatibility with the {@link ContextLoader} SPI, the
* default implementation simply delegates to {@link #processLocations()},
@@ -83,6 +69,7 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
* the supplied configuration attributes.
* <p>Can be overridden in subclasses &mdash; for example, to process
* configuration classes instead of resource locations.
* @since 3.1
* @see #processLocations()
*/
public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
@@ -91,13 +78,6 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
configAttributes.setLocations(processedLocations);
}
/**
* TODO Document default supports(MergedContextConfiguration) implementation.
*/
public boolean supports(MergedContextConfiguration mergedConfig) {
return !ObjectUtils.isEmpty(mergedConfig.getLocations()) && ObjectUtils.isEmpty(mergedConfig.getClasses());
}
// --- ContextLoader -------------------------------------------------------
/**
@@ -114,6 +94,7 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
* @param locations the unmodified locations to use for loading the
* application context (can be <code>null</code> or empty)
* @return a processed array of application context resource locations
* @since 2.5
* @see #isGenerateDefaultLocations()
* @see #generateDefaultLocations()
* @see #modifyLocations()
@@ -142,6 +123,7 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
* <em>default location generation</em> strategy.
* @param clazz the class for which the default locations are to be generated
* @return an array of default application context resource locations
* @since 2.5
* @see #getResourceSuffix()
*/
protected String[] generateDefaultLocations(Class<?> clazz) {
@@ -178,6 +160,7 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
* @param clazz the class with which the locations are associated
* @param locations the resource locations to be modified
* @return an array of modified application context resource locations
* @since 2.5
*/
protected String[] modifyLocations(Class<?> clazz, String... locations) {
String[] modifiedLocations = new String[locations.length];
@@ -203,6 +186,7 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
* {@link #processLocations()} are <code>null</code> or empty.
* <p>Can be overridden by subclasses to change the default behavior.
* @return always <code>true</code> by default
* @since 2.5
*/
protected boolean isGenerateDefaultLocations() {
return true;
@@ -213,6 +197,7 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
* locations when generating default locations.
* <p>Must be implemented by subclasses.
* @return the resource suffix; should not be <code>null</code> or empty
* @since 2.5
* @see #generateDefaultLocations()
*/
protected abstract String getResourceSuffix();

View File

@@ -86,6 +86,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* @return a new application context
* @see org.springframework.test.context.SmartContextLoader#loadContext(MergedContextConfiguration)
* @see GenericApplicationContext
* @since 3.1
*/
public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
if (logger.isDebugEnabled()) {
@@ -131,6 +132,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* @see org.springframework.test.context.ContextLoader#loadContext
* @see GenericApplicationContext
* @see #loadContext(MergedContextConfiguration)
* @since 2.5
*/
public final ConfigurableApplicationContext loadContext(String... locations) throws Exception {
if (logger.isDebugEnabled()) {
@@ -159,6 +161,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* @see GenericApplicationContext#setAllowBeanDefinitionOverriding
* @see GenericApplicationContext#setResourceLoader
* @see GenericApplicationContext#setId
* @since 2.5
*/
protected void prepareContext(GenericApplicationContext context) {
}
@@ -175,6 +178,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* @see DefaultListableBeanFactory#setAllowEagerClassLoading
* @see DefaultListableBeanFactory#setAllowCircularReferences
* @see DefaultListableBeanFactory#setAllowRawInjectionDespiteWrapping
* @since 2.5
*/
protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
}
@@ -194,8 +198,8 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* registering bean definitions.
* @param context the context into which the bean definitions should be loaded
* @param mergedConfig the merged context configuration
* @since 3.1
* @see #loadContext(MergedContextConfiguration)
* @since 3.1
*/
protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) {
createBeanDefinitionReader(context).loadBeanDefinitions(mergedConfig.getLocations());
@@ -210,6 +214,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* @see #loadContext(String...)
* @see #loadBeanDefinitions
* @see BeanDefinitionReader
* @since 2.5
*/
protected abstract BeanDefinitionReader createBeanDefinitionReader(GenericApplicationContext context);
@@ -222,6 +227,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
* @param context the newly created application context
* @see #loadContext(MergedContextConfiguration)
* @see #loadContext(String...)
* @since 2.5
*/
protected void customizeContext(GenericApplicationContext context) {
}

View File

@@ -64,31 +64,23 @@ public class AnnotationConfigContextLoader extends AbstractGenericContextLoader
/**
* Process configuration classes in the supplied {@link ContextConfigurationAttributes}.
* <p>If the configuration classes are <code>null</code> or empty and
* {@link #generatesDefaults()} returns <code>true</code>, this
* {@link #isGenerateDefaultLocations()} returns <code>true</code>, this
* <code>SmartContextLoader</code> will attempt to
* {@link #generateDefaultConfigurationClasses generate default configuration classes}.
* Otherwise, properties in the supplied configuration attributes will not
* be modified.
* @param configAttributes the context configuration attributes to process
* @see org.springframework.test.context.SmartContextLoader#processContextConfiguration()
* @see #generatesDefaults()
* @see #isGenerateDefaultLocations()
* @see #generateDefaultConfigurationClasses()
*/
public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
if (ObjectUtils.isEmpty(configAttributes.getClasses()) && generatesDefaults()) {
if (ObjectUtils.isEmpty(configAttributes.getClasses()) && isGenerateDefaultLocations()) {
Class<?>[] defaultConfigClasses = generateDefaultConfigurationClasses(configAttributes.getDeclaringClass());
configAttributes.setClasses(defaultConfigClasses);
}
}
/**
* TODO Document overridden supports(MergedContextConfiguration) implementation.
*/
@Override
public boolean supports(MergedContextConfiguration mergedConfig) {
return ObjectUtils.isEmpty(mergedConfig.getLocations()) && !ObjectUtils.isEmpty(mergedConfig.getClasses());
}
// --- AnnotationConfigContextLoader ---------------------------------------
private boolean isStaticNonPrivateAndNonFinal(Class<?> clazz) {
@@ -99,7 +91,7 @@ public class AnnotationConfigContextLoader extends AbstractGenericContextLoader
/**
* Determine if the supplied {@link Class} meets the criteria for being
* considered as a <em>default configuration class</em> candidate.
* considered a <em>default configuration class</em> candidate.
* <p>Specifically, such candidates:
* <ul>
* <li>must not be <code>null</code></li>

View File

@@ -27,6 +27,7 @@ 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.ObjectUtils;
/**
* TODO Document DelegatingSmartContextLoader.
@@ -41,92 +42,106 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
private static final Log logger = LogFactory.getLog(DelegatingSmartContextLoader.class);
private final List<? extends SmartContextLoader> candidates = Arrays.asList(new GenericXmlContextLoader(),
new AnnotationConfigContextLoader());
private final SmartContextLoader xmlLoader = new GenericXmlContextLoader();
private final SmartContextLoader annotationLoader = new AnnotationConfigContextLoader();
// --- SmartContextLoader --------------------------------------------------
/**
* TODO Document generatesDefaults() implementation.
*/
public boolean generatesDefaults() {
for (SmartContextLoader loader : candidates) {
if (loader.generatesDefaults()) {
return true;
}
private static String name(SmartContextLoader loader) {
return loader.getClass().getSimpleName();
}
private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Delegating to %s to process context configuration [%s].", name(loader),
configAttributes));
}
loader.processContextConfiguration(configAttributes);
}
private static boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) {
if (loader instanceof AnnotationConfigContextLoader) {
return ObjectUtils.isEmpty(mergedConfig.getLocations()) && !ObjectUtils.isEmpty(mergedConfig.getClasses());
}
else {
return !ObjectUtils.isEmpty(mergedConfig.getLocations()) && ObjectUtils.isEmpty(mergedConfig.getClasses());
}
return false;
}
/**
* TODO Document processContextConfiguration() implementation.
*/
public void processContextConfiguration(ContextConfigurationAttributes configAttributes) {
public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) {
final boolean originallyHadResources = configAttributes.hasResources();
if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
throw new IllegalStateException(String.format(
"Cannot process locations AND configuration classes for context "
+ "configuration [%s]; configure one or the other, but not both.", configAttributes));
}
// If the original locations and classes were not empty, there's no
// need to bother with default generation checks; just let each
// If the original locations or classes were not empty, there's no
// need to bother with default detection checks; just let the respective
// loader process the configuration.
if (originallyHadResources) {
for (SmartContextLoader loader : candidates) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Delegating to %s to process context configuration [%s].",
loader.getClass().getName(), configAttributes));
}
loader.processContextConfiguration(configAttributes);
}
if (configAttributes.hasLocations()) {
delegateProcessing(xmlLoader, configAttributes);
}
else if (generatesDefaults()) {
for (SmartContextLoader loader : candidates) {
boolean defaultResourcesAlreadyGenerated = configAttributes.hasResources();
// If defaults haven't already been generated and the loader
// claims to generate defaults, let it process the
// configuration.
if (!defaultResourcesAlreadyGenerated && loader.generatesDefaults()) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Delegating to %s to detect defaults for context configuration [%s].",
loader.getClass().getName(), configAttributes));
}
else if (configAttributes.hasClasses()) {
delegateProcessing(annotationLoader, configAttributes);
}
else {
// Else attempt to detect defaults...
loader.processContextConfiguration(configAttributes);
// Let the XML loader process the configuration.
delegateProcessing(xmlLoader, configAttributes);
boolean xmlLoaderDetectedDefaults = configAttributes.hasLocations();
if (configAttributes.hasResources()) {
if (logger.isInfoEnabled()) {
logger.info(String.format("SmartContextLoader candidate %s "
+ "detected defaults for context configuration [%s].", loader, configAttributes));
}
}
if (xmlLoaderDetectedDefaults) {
if (logger.isInfoEnabled()) {
logger.info(String.format("%s detected default locations for context configuration [%s].",
name(xmlLoader), configAttributes));
}
}
// If any loader claims to generate defaults but none actually did,
// throw an exception.
if (configAttributes.hasClasses()) {
throw new IllegalStateException(String.format(
"%s should NOT have detected default configuration classes for context configuration [%s].",
name(xmlLoader), configAttributes));
}
// Now let the annotation loader process the configuration.
delegateProcessing(annotationLoader, configAttributes);
if (configAttributes.hasClasses()) {
if (logger.isInfoEnabled()) {
logger.info(String.format(
"%s detected default configuration classes for context configuration [%s].",
name(annotationLoader), configAttributes));
}
}
if (!xmlLoaderDetectedDefaults && configAttributes.hasLocations()) {
throw new IllegalStateException(String.format(
"%s should NOT have detected default locations for context configuration [%s].",
name(annotationLoader), configAttributes));
}
// If neither loader detected defaults, throw an exception.
if (!configAttributes.hasResources()) {
throw new IllegalStateException(String.format("None of the SmartContextLoader candidates %s "
+ "was able to detect defaults for context configuration [%s].", candidates, configAttributes));
throw new IllegalStateException(String.format(
"Neither %s nor %s was able to detect defaults for context configuration [%s].", name(xmlLoader),
name(annotationLoader), configAttributes));
}
// TODO Throw exception if defaults were generated for both
// locations and configuration classes.
}
}
/**
* TODO Document supports(MergedContextConfiguration) implementation.
*/
public boolean supports(MergedContextConfiguration mergedConfig) {
Assert.notNull(mergedConfig, "mergedConfig must not be null");
for (SmartContextLoader loader : candidates) {
if (loader.supports(mergedConfig)) {
return true;
if (configAttributes.hasLocations() && configAttributes.hasClasses()) {
String message = String.format(
"Configuration error: both default locations AND default configuration classes "
+ "were detected for context configuration [%s]; configure one or the other, but not both.",
configAttributes);
logger.error(message);
throw new IllegalStateException(message);
}
}
return false;
}
/**
@@ -135,20 +150,23 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
Assert.notNull(mergedConfig, "mergedConfig must not be null");
List<SmartContextLoader> candidates = Arrays.asList(xmlLoader, annotationLoader);
// Determine if each loader can load a context from the mergedConfig. If
// it can, let it; otherwise, keep iterating.
for (SmartContextLoader loader : candidates) {
// Ask each loader if it can load a context from the mergedConfig.
// If it can, let it; otherwise, keep iterating.
if (loader.supports(mergedConfig)) {
if (supports(loader, mergedConfig)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Delegating to %s to load context from [%s].",
loader.getClass().getName(), mergedConfig));
logger.debug(String.format("Delegating to %s to load context from [%s].", name(loader),
mergedConfig));
}
return loader.loadContext(mergedConfig);
}
}
throw new IllegalStateException(String.format("None of the SmartContextLoader candidates %s "
+ "was able to load an ApplicationContext from [%s].", candidates, mergedConfig));
throw new IllegalStateException(String.format(
"Neither %s nor %s was able to load an ApplicationContext from [%s].", name(xmlLoader),
name(annotationLoader), mergedConfig));
}
// --- ContextLoader -------------------------------------------------------

View File

@@ -38,8 +38,8 @@ import org.springframework.test.context.support.GenericPropertiesContextLoader;
*/
public class ContextLoaderUtilsTests {
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[] {};
private static final String[] EMPTY_STRING_ARRAY = new String[] {};
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private void assertAttributes(ContextConfigurationAttributes attributes, Class<?> expectedDeclaringClass,
@@ -52,13 +52,23 @@ public class ContextLoaderUtilsTests {
assertEquals(expectedContextLoaderClass, attributes.getContextLoaderClass());
}
private void assertFooAttributes(ContextConfigurationAttributes attributes) {
assertAttributes(attributes, Foo.class, new String[] { "/foo.xml" }, new Class<?>[] { FooConfig.class },
private void assertLocationsFooAttributes(ContextConfigurationAttributes attributes) {
assertAttributes(attributes, LocationsFoo.class, new String[] { "/foo.xml" }, EMPTY_CLASS_ARRAY,
ContextLoader.class, false);
}
private void assertBarAttributes(ContextConfigurationAttributes attributes) {
assertAttributes(attributes, Bar.class, new String[] { "/bar.xml" }, new Class<?>[] { BarConfig.class },
private void assertClassesFooAttributes(ContextConfigurationAttributes attributes) {
assertAttributes(attributes, ClassesFoo.class, EMPTY_STRING_ARRAY, new Class<?>[] { FooConfig.class },
ContextLoader.class, false);
}
private void assertLocationsBarAttributes(ContextConfigurationAttributes attributes) {
assertAttributes(attributes, LocationsBar.class, new String[] { "/bar.xml" }, EMPTY_CLASS_ARRAY,
AnnotationConfigContextLoader.class, true);
}
private void assertClassesBarAttributes(ContextConfigurationAttributes attributes) {
assertAttributes(attributes, ClassesBar.class, EMPTY_STRING_ARRAY, new Class<?>[] { BarConfig.class },
AnnotationConfigContextLoader.class, true);
}
@@ -90,20 +100,37 @@ public class ContextLoaderUtilsTests {
}
@Test
public void resolveContextConfigurationAttributesWithLocalAnnotation() {
List<ContextConfigurationAttributes> attributesList = ContextLoaderUtils.resolveContextConfigurationAttributes(Foo.class);
public void resolveContextConfigurationAttributesWithLocalAnnotationAndLocations() {
List<ContextConfigurationAttributes> attributesList = ContextLoaderUtils.resolveContextConfigurationAttributes(LocationsFoo.class);
assertNotNull(attributesList);
assertEquals(1, attributesList.size());
assertFooAttributes(attributesList.get(0));
assertLocationsFooAttributes(attributesList.get(0));
}
@Test
public void resolveContextConfigurationAttributesWithLocalAndInheritedAnnotations() {
List<ContextConfigurationAttributes> attributesList = ContextLoaderUtils.resolveContextConfigurationAttributes(Bar.class);
public void resolveContextConfigurationAttributesWithLocalAnnotationAndClasses() {
List<ContextConfigurationAttributes> attributesList = ContextLoaderUtils.resolveContextConfigurationAttributes(ClassesFoo.class);
assertNotNull(attributesList);
assertEquals(1, attributesList.size());
assertClassesFooAttributes(attributesList.get(0));
}
@Test
public void resolveContextConfigurationAttributesWithLocalAndInheritedAnnotationsAndLocations() {
List<ContextConfigurationAttributes> attributesList = ContextLoaderUtils.resolveContextConfigurationAttributes(LocationsBar.class);
assertNotNull(attributesList);
assertEquals(2, attributesList.size());
assertFooAttributes(attributesList.get(0));
assertBarAttributes(attributesList.get(1));
assertLocationsFooAttributes(attributesList.get(0));
assertLocationsBarAttributes(attributesList.get(1));
}
@Test
public void resolveContextConfigurationAttributesWithLocalAndInheritedAnnotationsAndClasses() {
List<ContextConfigurationAttributes> attributesList = ContextLoaderUtils.resolveContextConfigurationAttributes(ClassesBar.class);
assertNotNull(attributesList);
assertEquals(2, attributesList.size());
assertClassesFooAttributes(attributesList.get(0));
assertClassesBarAttributes(attributesList.get(1));
}
@Test(expected = IllegalArgumentException.class)
@@ -124,33 +151,62 @@ public class ContextLoaderUtilsTests {
}
@Test
public void buildMergedContextConfigurationWithLocalAnnotation() {
Class<?> testClass = Foo.class;
public void buildMergedContextConfigurationWithLocalAnnotationAndLocations() {
Class<?> testClass = LocationsFoo.class;
MergedContextConfiguration mergedConfig = ContextLoaderUtils.buildMergedContextConfiguration(testClass, null);
assertMergedContextConfiguration(mergedConfig, testClass, new String[] { "classpath:/foo.xml" },
EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class);
}
@Test
public void buildMergedContextConfigurationWithLocalAnnotationAndClasses() {
Class<?> testClass = ClassesFoo.class;
MergedContextConfiguration mergedConfig = ContextLoaderUtils.buildMergedContextConfiguration(testClass, null);
assertMergedContextConfiguration(mergedConfig, testClass, EMPTY_STRING_ARRAY,
new Class<?>[] { FooConfig.class }, DelegatingSmartContextLoader.class);
}
@Test
public void buildMergedContextConfigurationWithLocalAnnotationAndOverriddenContextLoader() {
Class<?> testClass = Foo.class;
public void buildMergedContextConfigurationWithLocalAnnotationAndOverriddenContextLoaderAndLocations() {
Class<?> testClass = LocationsFoo.class;
Class<? extends ContextLoader> expectedContextLoaderClass = GenericPropertiesContextLoader.class;
MergedContextConfiguration mergedConfig = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
expectedContextLoaderClass.getName());
assertMergedContextConfiguration(mergedConfig, testClass, new String[] { "classpath:/foo.xml" },
EMPTY_CLASS_ARRAY, expectedContextLoaderClass);
}
@Test
public void buildMergedContextConfigurationWithLocalAnnotationAndOverriddenContextLoaderAndClasses() {
Class<?> testClass = ClassesFoo.class;
Class<? extends ContextLoader> expectedContextLoaderClass = GenericPropertiesContextLoader.class;
MergedContextConfiguration mergedConfig = ContextLoaderUtils.buildMergedContextConfiguration(testClass,
expectedContextLoaderClass.getName());
assertMergedContextConfiguration(mergedConfig, testClass, EMPTY_STRING_ARRAY,
new Class<?>[] { FooConfig.class }, expectedContextLoaderClass);
}
@Test
public void buildMergedContextConfigurationWithLocalAndInheritedAnnotations() {
Class<?> testClass = Bar.class;
public void buildMergedContextConfigurationWithLocalAndInheritedAnnotationsAndLocations() {
Class<?> testClass = LocationsBar.class;
String[] expectedLocations = new String[] { "/foo.xml", "/bar.xml" };
MergedContextConfiguration mergedConfig = ContextLoaderUtils.buildMergedContextConfiguration(testClass, null);
assertMergedContextConfiguration(mergedConfig, testClass, expectedLocations, EMPTY_CLASS_ARRAY,
AnnotationConfigContextLoader.class);
}
@Test
public void buildMergedContextConfigurationWithLocalAndInheritedAnnotationsAndClasses() {
Class<?> testClass = ClassesBar.class;
Class<?>[] expectedClasses = new Class<?>[] { FooConfig.class, BarConfig.class };
MergedContextConfiguration mergedConfig = ContextLoaderUtils.buildMergedContextConfiguration(testClass, null);
assertMergedContextConfiguration(mergedConfig, testClass, expectedLocations, expectedClasses,
assertMergedContextConfiguration(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses,
AnnotationConfigContextLoader.class);
}
@@ -186,21 +242,28 @@ public class ContextLoaderUtilsTests {
@Test
public void resolveActiveProfilesWithLocalAnnotation() {
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(Foo.class);
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(LocationsFoo.class);
assertNotNull(profiles);
assertArrayEquals(new String[] { "foo" }, profiles);
}
@Test
public void resolveActiveProfilesWithInheritedAnnotation() {
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(InheritedFoo.class);
public void resolveActiveProfilesWithInheritedAnnotationAndLocations() {
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(InheritedLocationsFoo.class);
assertNotNull(profiles);
assertArrayEquals(new String[] { "foo" }, profiles);
}
@Test
public void resolveActiveProfilesWithInheritedAnnotationAndClasses() {
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(InheritedClassesFoo.class);
assertNotNull(profiles);
assertArrayEquals(new String[] { "foo" }, profiles);
}
@Test
public void resolveActiveProfilesWithLocalAndInheritedAnnotations() {
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(Bar.class);
String[] profiles = ContextLoaderUtils.resolveActiveProfiles(LocationsBar.class);
assertNotNull(profiles);
assertEquals(2, profiles.length);
@@ -245,25 +308,38 @@ public class ContextLoaderUtilsTests {
private static class FooConfig {
}
@ContextConfiguration(locations = "/foo.xml", classes = FooConfig.class, inheritLocations = false)
@ContextConfiguration(locations = "/foo.xml", inheritLocations = false)
@ActiveProfiles(profiles = "foo")
private static class Foo {
private static class LocationsFoo {
}
private static class InheritedFoo extends Foo {
@ContextConfiguration(classes = FooConfig.class, inheritLocations = false)
@ActiveProfiles(profiles = "foo")
private static class ClassesFoo {
}
private static class InheritedLocationsFoo extends LocationsFoo {
}
private static class InheritedClassesFoo extends ClassesFoo {
}
@Configuration
private static class BarConfig {
}
@ContextConfiguration(locations = "/bar.xml", classes = BarConfig.class, inheritLocations = true, loader = AnnotationConfigContextLoader.class)
@ContextConfiguration(locations = "/bar.xml", inheritLocations = true, loader = AnnotationConfigContextLoader.class)
@ActiveProfiles("bar")
private static class Bar extends Foo {
private static class LocationsBar extends LocationsFoo {
}
@ContextConfiguration(classes = BarConfig.class, inheritLocations = true, loader = AnnotationConfigContextLoader.class)
@ActiveProfiles("bar")
private static class ClassesBar extends ClassesFoo {
}
@ActiveProfiles(profiles = { "dog", "cat" }, inheritProfiles = false)
private static class Animals extends Bar {
private static class Animals extends LocationsBar {
}
}

View File

@@ -37,6 +37,7 @@ import org.springframework.test.context.support.AnnotationConfigContextLoader;
*
* @author Sam Brannen
* @since 3.1
* @see DefaultLoaderDefaultConfigClassesBaseTests
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)

View File

@@ -36,6 +36,7 @@ import org.springframework.test.context.support.DelegatingSmartContextLoader;
*
* @author Sam Brannen
* @since 3.1
* @see DefaultConfigClassesBaseTests
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration

View File

@@ -18,7 +18,6 @@ package org.springframework.test.context.support;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -50,12 +49,7 @@ public class DelegatingSmartContextLoaderTests {
assertTrue(ObjectUtils.isEmpty(array));
}
// --- SmartContextLoader --------------------------------------------------
@Test
public void generatesDefaults() {
assertTrue(loader.generatesDefaults());
}
// --- SmartContextLoader - processContextConfiguration() ------------------
@Test(expected = IllegalStateException.class)
public void processContextConfigurationWithoutLocationsAndConfigurationClassesForBogusTestClass() {
@@ -82,6 +76,14 @@ public class DelegatingSmartContextLoaderTests {
assertEmpty(configAttributes.getLocations());
}
@Test(expected = IllegalStateException.class)
public void processContextConfigurationWithDefaultXmlConfigAndConfigurationClassGeneration() {
ContextConfigurationAttributes configAttributes = new ContextConfigurationAttributes(
ImproperDuplicateDefaultXmlAndConfigClassTestCase.class, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, true,
ContextLoader.class);
loader.processContextConfiguration(configAttributes);
}
@Test
public void processContextConfigurationWithLocation() {
String[] locations = new String[] { "classpath:/foo.xml" };
@@ -102,39 +104,7 @@ public class DelegatingSmartContextLoaderTests {
assertEmpty(configAttributes.getLocations());
}
@Test(expected = IllegalArgumentException.class)
public void doesNotSupportNullConfig() {
MergedContextConfiguration mergedConfig = null;
loader.supports(mergedConfig);
}
@Test
public void doesNotSupportEmptyConfig() {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertFalse(loader.supports(mergedConfig));
}
@Test
public void doesNotSupportLocationsAndConfigurationClasses() {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(),
new String[] { "foo.xml" }, new Class<?>[] { getClass() }, EMPTY_STRING_ARRAY, loader);
assertFalse(loader.supports(mergedConfig));
}
@Test
public void supportsLocations() {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(),
new String[] { "foo.xml" }, EMPTY_CLASS_ARRAY, EMPTY_STRING_ARRAY, loader);
assertTrue(loader.supports(mergedConfig));
}
@Test
public void supportsConfigurationClasses() {
MergedContextConfiguration mergedConfig = new MergedContextConfiguration(getClass(), EMPTY_STRING_ARRAY,
new Class<?>[] { getClass() }, EMPTY_STRING_ARRAY, loader);
assertTrue(loader.supports(mergedConfig));
}
// --- SmartContextLoader - loadContext() ----------------------------------
@Test(expected = IllegalArgumentException.class)
public void loadContextWithNullConfig() throws Exception {
@@ -187,6 +157,8 @@ public class DelegatingSmartContextLoaderTests {
}
// -------------------------------------------------------------------------
static class XmlTestCase {
}
@@ -202,4 +174,13 @@ public class DelegatingSmartContextLoaderTests {
}
}
static class ImproperDuplicateDefaultXmlAndConfigClassTestCase {
@Configuration
static class Config {
// intentionally empty: we just need the class to be present to fail
// the test
}
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<!-- intentionally empty: we just need the file to be present to fail the test -->
</beans>