Support ApplicationContextInitializers in the TCF

Starting with Spring 3.1 applications can specify
contextInitializerClasses via context-param and init-param in web.xml;
however, there is currently no way to have such initializers invoked in
integration testing scenarios without writing a custom
SmartContextLoader. For comprehensive integration testing it should
therefore be possible to re-use ApplicationContextInitializers in the
Spring TestContext Framework as well.

This commit makes this possible at the @ContextConfiguration level by
allowing an array of ACI types to be specified, and the out-of-the-box
SmartContextLoader implementations invoke the declared initializers at
the appropriate time.

 - Added initializers and inheritInitializers attributes to
   @ContextConfiguration.

 - Introduced support for ApplicationContextInitializers in
   ContextConfigurationAttributes, MergedContextConfiguration, and
   ContextLoaderUtils.

 - MergedContextConfiguration stores context initializer classes as a
   Set and incorporates them into the implementations of hashCode() and
   equals() for proper context caching.

 - ApplicationContextInitializers are invoked in the new
   prepareContext(GenericApplicationContext, MergedContextConfiguration)
   method in AbstractGenericContextLoader, and ordering declared via the
   Ordered interface and @Order annotation is honored.

 - Updated DelegatingSmartContextLoader to support initializers.
   Specifically, a test class may optionally declare neither XML
   configuration files nor annotated classes and instead declare only
   application context initializers. In such cases, an attempt will
   still be made to detect defaults, but their absence will not result
   an an exception.

 - Documented support for application context initializers in Javadoc
   and in the testing chapter of the reference manual.

Issue: SPR-9011
This commit is contained in:
Sam Brannen
2012-08-09 19:05:08 +02:00
parent 9c8c967caa
commit 1f93777bbd
24 changed files with 1595 additions and 260 deletions

View File

@@ -23,19 +23,19 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
/**
* {@code ContextConfiguration} defines class-level metadata that is
* {@code @ContextConfiguration} defines class-level metadata that is
* used to determine how to load and configure an
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* for test classes.
* for integration tests.
*
* <h3>Supported Resource Types</h3>
*
* <p>Prior to Spring 3.1, only path-based resource locations were supported.
* As of Spring 3.1, {@link #loader context loaders} may choose to support
* As of Spring 3.1, {@linkplain #loader context loaders} may choose to support
* either path-based or class-based resources (but not both). Consequently
* {@code @ContextConfiguration} can be used to declare either path-based
* resource locations (via the {@link #locations} or {@link #value}
@@ -47,16 +47,20 @@ import org.springframework.context.annotation.Configuration;
* <p>The term <em>annotated class</em> can refer to any of the following.
*
* <ul>
* <li>A class annotated with {@link Configuration @Configuration}</li>
* <li>A class annotated with
* {@link org.springframework.context.annotation.Configuration @Configuration}</li>
* <li>A component (i.e., a class annotated with
* {@link org.springframework.stereotype.Component @Component},
* {@link org.springframework.stereotype.Service @Service},
* {@link org.springframework.stereotype.Repository @Repository}, etc.)</li>
* <li>A JSR-330 compliant class that is annotated with {@code javax.inject} annotations</li>
* <li>Any other class that contains {@link Bean @Bean}-methods</li>
* <li>Any other class that contains
* {@link org.springframework.context.annotation.Bean @Bean}-methods</li>
* </ul>
*
* Consult the JavaDoc for {@link Configuration @Configuration} and {@link Bean @Bean}
* Consult the Javadoc for
* {@link org.springframework.context.annotation.Configuration @Configuration} and
* {@link org.springframework.context.annotation.Bean @Bean}
* for further information regarding the configuration and semantics of
* <em>annotated classes</em>.
*
@@ -66,8 +70,8 @@ import org.springframework.context.annotation.Configuration;
* @see SmartContextLoader
* @see ContextConfigurationAttributes
* @see MergedContextConfiguration
* @see org.springframework.context.ApplicationContext
* @see ActiveProfiles
* @see org.springframework.context.ApplicationContext
*/
@Documented
@Inherited
@@ -82,6 +86,7 @@ public @interface ContextConfiguration {
* with {@link #locations} or {@link #classes}, but it may be used
* instead of {@link #locations}.
* @since 3.0
* @see #inheritLocations
*/
String[] value() default {};
@@ -111,6 +116,7 @@ public @interface ContextConfiguration {
* {@link #value} or {@link #classes}, but it may be used instead of
* {@link #value}.
* @since 2.5
* @see #inheritLocations
*/
String[] locations() default {};
@@ -131,9 +137,31 @@ public @interface ContextConfiguration {
* @since 3.1
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.test.context.support.AnnotationConfigContextLoader
* @see #inheritLocations
*/
Class<?>[] classes() default {};
/**
* The application context <em>initializer classes</em> to use for initializing
* a {@link ConfigurableApplicationContext}.
*
* <p>The concrete {@code ConfigurableApplicationContext} type supported by each
* declared initializer must be compatible with the type of {@code ApplicationContext}
* created by the {@link SmartContextLoader} in use.
*
* <p>{@code SmartContextLoader} implementations typically detect whether
* Spring's {@link org.springframework.core.Ordered Ordered} interface has been
* implemented or if the @{@link org.springframework.core.annotation.Order Order}
* annotation is present and sort instances accordingly prior to invoking them.
*
* @since 3.2
* @see org.springframework.context.ApplicationContextInitializer
* @see org.springframework.context.ConfigurableApplicationContext
* @see #inheritInitializers
* @see #loader
*/
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers() default {};
/**
* Whether or not {@link #locations resource locations} or <em>annotated
* classes</em> from test superclasses should be <em>inherited</em>.
@@ -194,7 +222,45 @@ public @interface ContextConfiguration {
boolean inheritLocations() default true;
/**
* The type of {@link ContextLoader} (or {@link SmartContextLoader}) to use
* Whether or not {@linkplain #initializers context initializers} from test
* superclasses should be <em>inherited</em>.
*
* <p>The default value is <code>true</code>. This means that an annotated
* class will <em>inherit</em> the application context initializers defined
* by test superclasses. Specifically, the initializers for a given test
* class will be added to the set of initializers defined by test
* superclasses. Thus, subclasses have the option of <em>extending</em> the
* set of initializers.
*
* <p>If <code>inheritInitializers</code> is set to <code>false</code>, the
* initializers for the annotated class will <em>shadow</em> and effectively
* replace any initializers defined by superclasses.
*
* <p>In the following example, the
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* for {@code ExtendedTest} will be initialized using
* {@code BaseInitializer} <strong>and</strong> {@code ExtendedInitializer}.
* Note, however, that the order in which the initializers are invoked
* depends on whether they implement {@link org.springframework.core.Ordered
* Ordered} or are annotated with {@link org.springframework.core.annotation.Order
* &#064;Order}.
* <pre class="code">
* &#064;ContextConfiguration(initializers = BaseInitializer.class)
* public class BaseTest {
* // ...
* }
*
* &#064;ContextConfiguration(initializers = ExtendedInitializer.class)
* public class ExtendedTest extends BaseTest {
* // ...
* }
* </pre>
* @since 3.2
*/
boolean inheritInitializers() default true;
/**
* The type of {@link SmartContextLoader} (or {@link ContextLoader}) to use
* for loading an {@link org.springframework.context.ApplicationContext
* ApplicationContext}.
*

View File

@@ -18,12 +18,15 @@ package org.springframework.test.context;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* <code>ContextConfigurationAttributes</code> encapsulates the context
* {@code ContextConfigurationAttributes} encapsulates the context
* configuration attributes declared on a test class via
* {@link ContextConfiguration @ContextConfiguration}.
*
@@ -47,6 +50,10 @@ public class ContextConfigurationAttributes {
private final Class<? extends ContextLoader> contextLoaderClass;
private final Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers;
private final boolean inheritInitializers;
/**
* Resolve resource locations from the {@link ContextConfiguration#locations() locations}
@@ -68,8 +75,7 @@ public class ContextConfigurationAttributes {
ObjectUtils.nullSafeToString(valueLocations), ObjectUtils.nullSafeToString(locations));
logger.error(msg);
throw new IllegalStateException(msg);
}
else if (!ObjectUtils.isEmpty(valueLocations)) {
} else if (!ObjectUtils.isEmpty(valueLocations)) {
locations = valueLocations;
}
@@ -79,31 +85,59 @@ public class ContextConfigurationAttributes {
/**
* Construct a new {@link ContextConfigurationAttributes} instance for the
* supplied {@link ContextConfiguration @ContextConfiguration} annotation and
* the {@link Class test class} that declared it.
* the {@linkplain Class test class} that declared it.
* @param declaringClass the test class that declared {@code @ContextConfiguration}
* @param contextConfiguration the annotation from which to retrieve the attributes
*/
public ContextConfigurationAttributes(Class<?> declaringClass, ContextConfiguration contextConfiguration) {
this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(),
contextConfiguration.inheritLocations(), contextConfiguration.loader());
contextConfiguration.inheritLocations(), contextConfiguration.initializers(),
contextConfiguration.inheritInitializers(), contextConfiguration.loader());
}
/**
* Construct a new {@link ContextConfigurationAttributes} instance for the
* {@link Class test class} that declared the
* {@linkplain Class test class} that declared the
* {@link ContextConfiguration @ContextConfiguration} annotation and its
* corresponding attributes.
*
* @param declaringClass the test class that declared {@code @ContextConfiguration}
* @param locations the resource locations declared via {@code @ContextConfiguration}
* @param classes the annotated classes declared via {@code @ContextConfiguration}
* @param inheritLocations the <code>inheritLocations</code> flag declared via {@code @ContextConfiguration}
* @param inheritLocations the {@code inheritLocations} 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
* {@code null}, or if the {@code locations} and {@code classes} are both non-empty
* @deprecated as of Spring 3.2, use
* {@link #ContextConfigurationAttributes(Class, String[], Class[], boolean, Class[], boolean, Class)}
* instead
*/
@Deprecated
public ContextConfigurationAttributes(Class<?> declaringClass, String[] locations, Class<?>[] classes,
boolean inheritLocations, Class<? extends ContextLoader> contextLoaderClass) {
this(declaringClass, locations, classes, inheritLocations, null, true, contextLoaderClass);
}
/**
* Construct a new {@link ContextConfigurationAttributes} instance for the
* {@linkplain Class test class} that declared the
* {@link ContextConfiguration @ContextConfiguration} annotation and its
* corresponding attributes.
*
* @param declaringClass the test class that declared {@code @ContextConfiguration}
* @param locations the resource locations declared via {@code @ContextConfiguration}
* @param classes the annotated classes declared via {@code @ContextConfiguration}
* @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration}
* @param initializers the context initializers declared via {@code @ContextConfiguration}
* @param inheritInitializers the {@code inheritInitializers} 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}, or if the {@code locations} and {@code classes} are both non-empty
*/
public ContextConfigurationAttributes(Class<?> declaringClass, String[] locations, Class<?>[] classes,
boolean inheritLocations,
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers,
boolean inheritInitializers, Class<? extends ContextLoader> contextLoaderClass) {
Assert.notNull(declaringClass, "declaringClass must not be null");
Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null");
@@ -122,14 +156,16 @@ public class ContextConfigurationAttributes {
this.locations = locations;
this.classes = classes;
this.inheritLocations = inheritLocations;
this.initializers = initializers;
this.inheritInitializers = inheritInitializers;
this.contextLoaderClass = contextLoaderClass;
}
/**
* Get the {@link Class class} that declared the
* Get the {@linkplain Class class} that declared the
* {@link ContextConfiguration @ContextConfiguration} annotation.
*
* @return the declaring class; never <code>null</code>
* @return the declaring class; never {@code null}
*/
public Class<?> getDeclaringClass() {
return declaringClass;
@@ -143,7 +179,7 @@ public class ContextConfigurationAttributes {
* represent a <em>processed</em> value that does not match the original value
* declared via {@link ContextConfiguration @ContextConfiguration}.
*
* @return the resource locations; potentially <code>null</code> or <em>empty</em>
* @return the resource locations; potentially {@code null} or <em>empty</em>
* @see ContextConfiguration#value
* @see ContextConfiguration#locations
* @see #setLocations(String[])
@@ -170,7 +206,7 @@ public class ContextConfigurationAttributes {
* represent a <em>processed</em> value that does not match the original value
* declared via {@link ContextConfiguration @ContextConfiguration}.
*
* @return the annotated classes; potentially <code>null</code> or <em>empty</em>
* @return the annotated classes; potentially {@code null} or <em>empty</em>
* @see ContextConfiguration#classes
* @see #setClasses(Class[])
*/
@@ -192,7 +228,7 @@ public class ContextConfigurationAttributes {
* Determine if this {@code ContextConfigurationAttributes} instance has
* path-based resource locations.
*
* @return <code>true</code> if the {@link #getLocations() locations} array is not empty
* @return {@code true} if the {@link #getLocations() locations} array is not empty
* @see #hasResources()
* @see #hasClasses()
*/
@@ -204,7 +240,7 @@ public class ContextConfigurationAttributes {
* Determine if this {@code ContextConfigurationAttributes} instance has
* class-based resources.
*
* @return <code>true</code> if the {@link #getClasses() classes} array is not empty
* @return {@code true} if the {@link #getClasses() classes} array is not empty
* @see #hasResources()
* @see #hasLocations()
*/
@@ -216,7 +252,7 @@ public class ContextConfigurationAttributes {
* 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}
* @return {@code true} if either the {@link #getLocations() locations}
* or the {@link #getClasses() classes} array is not empty
* @see #hasLocations()
* @see #hasClasses()
@@ -226,10 +262,10 @@ public class ContextConfigurationAttributes {
}
/**
* Get the <code>inheritLocations</code> flag that was declared via
* Get the {@code inheritLocations} flag that was declared via
* {@link ContextConfiguration @ContextConfiguration}.
*
* @return the <code>inheritLocations</code> flag
* @return the {@code inheritLocations} flag
* @see ContextConfiguration#inheritLocations
*/
public boolean isInheritLocations() {
@@ -237,10 +273,32 @@ public class ContextConfigurationAttributes {
}
/**
* Get the <code>ContextLoader</code> class that was declared via
* Get the {@code ApplicationContextInitializer} classes that were declared via
* {@link ContextConfiguration @ContextConfiguration}.
*
* @return the <code>ContextLoader</code> class
* @return the {@code ApplicationContextInitializer} classes
* @since 3.2
*/
public Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] getInitializers() {
return initializers;
}
/**
* Get the {@code inheritInitializers} flag that was declared via
* {@link ContextConfiguration @ContextConfiguration}.
*
* @return the {@code inheritInitializers} flag
* @since 3.2
*/
public boolean isInheritInitializers() {
return inheritInitializers;
}
/**
* Get the {@code ContextLoader} class that was declared via
* {@link ContextConfiguration @ContextConfiguration}.
*
* @return the {@code ContextLoader} class
* @see ContextConfiguration#loader
*/
public Class<? extends ContextLoader> getContextLoaderClass() {
@@ -258,6 +316,8 @@ public class ContextConfigurationAttributes {
.append("locations", ObjectUtils.nullSafeToString(locations))//
.append("classes", ObjectUtils.nullSafeToString(classes))//
.append("inheritLocations", inheritLocations)//
.append("initializers", ObjectUtils.nullSafeToString(initializers))//
.append("inheritInitializers", inheritInitializers)//
.append("contextLoaderClass", contextLoaderClass.getName())//
.toString();
}

View File

@@ -23,8 +23,8 @@ import org.springframework.context.ApplicationContext;
* for an integration test managed by the Spring TestContext Framework.
*
* <p><b>Note</b>: as of Spring 3.1, implement {@link SmartContextLoader} instead
* of this interface in order to provide support for annotated classes and active
* bean definition profiles.
* of this interface in order to provide support for annotated classes, active
* bean definition profiles, and application context initializers.
*
* <p>Clients of a ContextLoader should call
* {@link #processLocations(Class,String...) processLocations()} prior to

View File

@@ -21,12 +21,15 @@ import static org.springframework.core.annotation.AnnotationUtils.findAnnotation
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -156,8 +159,7 @@ abstract class ContextLoaderUtils {
}
return (Class<? extends ContextLoader>) ContextLoaderUtils.class.getClassLoader().loadClass(
defaultContextLoaderClassName);
}
catch (ClassNotFoundException ex) {
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("Could not load default ContextLoader class ["
+ defaultContextLoaderClassName + "]. Specify @ContextConfiguration's 'loader' "
+ "attribute or make the default loader class available.");
@@ -169,17 +171,15 @@ abstract class ContextLoaderUtils {
* attributes} for the supplied {@link Class class} and its superclasses.
*
* <p>Note that the {@link ContextConfiguration#inheritLocations
* inheritLocations} flag of {@link ContextConfiguration
* &#064;ContextConfiguration} will be taken into consideration.
* Specifically, if the <code>inheritLocations</code> flag is set to
* <code>true</code>, configuration attributes defined in the test
* class will be appended to the configuration attributes defined in
* superclasses.
* inheritLocations} and {@link ContextConfiguration#inheritInitializers()
* inheritInitializers} flags of {@link ContextConfiguration
* &#064;ContextConfiguration} will <strong>not</strong> be taken into
* consideration. If these flags need to be honored, that must be handled
* manually when traversing the list returned by this method.
*
* @param clazz the class for which to resolve the configuration attributes (must
* not be <code>null</code>)
* @return the list of configuration attributes for the specified class,
* including configuration attributes from superclasses if appropriate
* @return the list of configuration attributes for the specified class
* (never <code>null</code>)
* @throws IllegalArgumentException if the supplied class is <code>null</code> or
* if {@code @ContextConfiguration} is not <em>present</em> on the supplied class
@@ -211,13 +211,69 @@ abstract class ContextLoaderUtils {
attributesList.add(0, attributes);
declaringClass = contextConfiguration.inheritLocations() ? findAnnotationDeclaringClass(annotationType,
declaringClass.getSuperclass()) : null;
declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass());
}
return attributesList;
}
/**
* Create a copy of the supplied list of {@code ContextConfigurationAttributes}
* in reverse order.
*
* @since 3.2
*/
private static List<ContextConfigurationAttributes> reverseContextConfigurationAttributes(
List<ContextConfigurationAttributes> configAttributesList) {
List<ContextConfigurationAttributes> configAttributesListReversed = new ArrayList<ContextConfigurationAttributes>(
configAttributesList);
Collections.reverse(configAttributesListReversed);
return configAttributesListReversed;
}
/**
* Resolve the list of merged {@code ApplicationContextInitializer} classes
* for the supplied list of {@code ContextConfigurationAttributes}.
*
* <p>Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers}
* flag of {@link ContextConfiguration @ContextConfiguration} will be taken into
* consideration. Specifically, if the <code>inheritInitializers</code> flag is
* set to <code>true</code> for a given level in the class hierarchy represented by
* the provided configuration attributes, context initializer classes defined
* at the given level will be merged with those defined in higher levels
* of the class hierarchy.
*
* @param configAttributesList the list of configuration attributes to process
* (must not be <code>null</code>)
* @return the list of merged context initializer classes, including those
* from superclasses if appropriate (never <code>null</code>)
* @since 3.2
*/
static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> resolveInitializerClasses(
List<ContextConfigurationAttributes> configAttributesList) {
Assert.notNull(configAttributesList, "configAttributesList must not be null");
final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = //
new HashSet<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>>();
// Traverse config attributes in reverse order (i.e., as if we were traversing up
// the class hierarchy).
for (ContextConfigurationAttributes configAttributes : reverseContextConfigurationAttributes(configAttributesList)) {
if (logger.isTraceEnabled()) {
logger.trace(String.format("Processing context initializers for context configuration attributes %s",
configAttributes));
}
initializerClasses.addAll(Arrays.asList(configAttributes.getInitializers()));
if (!configAttributes.isInheritInitializers()) {
break;
}
}
return initializerClasses;
}
/**
* Resolve <em>active bean definition profiles</em> for the supplied {@link Class}.
*
@@ -266,8 +322,7 @@ abstract class ContextLoaderUtils {
ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles));
logger.error(msg);
throw new IllegalStateException(msg);
}
else if (!ObjectUtils.isEmpty(valueProfiles)) {
} else if (!ObjectUtils.isEmpty(valueProfiles)) {
profiles = valueProfiles;
}
@@ -309,31 +364,38 @@ abstract class ContextLoaderUtils {
final List<String> locationsList = new ArrayList<String>();
final List<Class<?>> classesList = new ArrayList<Class<?>>();
for (ContextConfigurationAttributes configAttributes : configAttributesList) {
// Traverse config attributes in reverse order (i.e., as if we were traversing up
// the class hierarchy).
for (ContextConfigurationAttributes configAttributes : reverseContextConfigurationAttributes(configAttributesList)) {
if (logger.isTraceEnabled()) {
logger.trace(String.format(
"Processing locations and classes for context configuration attributes [%s]", configAttributes));
logger.trace(String.format("Processing locations and classes for context configuration attributes %s",
configAttributes));
}
if (contextLoader instanceof SmartContextLoader) {
SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader;
smartContextLoader.processContextConfiguration(configAttributes);
locationsList.addAll(Arrays.asList(configAttributes.getLocations()));
classesList.addAll(Arrays.asList(configAttributes.getClasses()));
}
else {
locationsList.addAll(0, Arrays.asList(configAttributes.getLocations()));
classesList.addAll(0, Arrays.asList(configAttributes.getClasses()));
} else {
String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(),
configAttributes.getLocations());
locationsList.addAll(Arrays.asList(processedLocations));
locationsList.addAll(0, Arrays.asList(processedLocations));
// Legacy ContextLoaders don't know how to process classes
}
if (!configAttributes.isInheritLocations()) {
break;
}
}
String[] locations = StringUtils.toStringArray(locationsList);
Class<?>[] classes = ClassUtils.toClassArray(classesList);
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = resolveInitializerClasses(configAttributesList);
String[] activeProfiles = resolveActiveProfiles(testClass);
return new MergedContextConfiguration(testClass, locations, classes, activeProfiles, contextLoader);
return new MergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles,
contextLoader);
}
}

View File

@@ -18,9 +18,13 @@ package org.springframework.test.context;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -59,10 +63,13 @@ public class MergedContextConfiguration implements Serializable {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
private static final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> EMPTY_INITIALIZER_CLASSES = //
Collections.<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> emptySet();
private final Class<?> testClass;
private final String[] locations;
private final Class<?>[] classes;
private final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses;
private final String[] activeProfiles;
private final ContextLoader contextLoader;
@@ -75,6 +82,12 @@ public class MergedContextConfiguration implements Serializable {
return classes == null ? EMPTY_CLASS_ARRAY : classes;
}
private static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> processContextInitializerClasses(
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses) {
return contextInitializerClasses == null ? EMPTY_INITIALIZER_CLASSES
: Collections.unmodifiableSet(contextInitializerClasses);
}
private static String[] processActiveProfiles(String[] activeProfiles) {
if (activeProfiles == null) {
return EMPTY_STRING_ARRAY;
@@ -111,46 +124,84 @@ public class MergedContextConfiguration implements Serializable {
* @param classes the merged annotated classes
* @param activeProfiles the merged active bean definition profiles
* @param contextLoader the resolved <code>ContextLoader</code>
* @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader)
*/
public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
String[] activeProfiles, ContextLoader contextLoader) {
this(testClass, locations, classes, null, activeProfiles, contextLoader);
}
/**
* Create a new {@code MergedContextConfiguration} instance for the
* supplied test class, resource locations, annotated classes, context
* initializers, active profiles, and {@code ContextLoader}.
*
* <p>If a <code>null</code> value is supplied for <code>locations</code>,
* <code>classes</code>, or <code>activeProfiles</code> an empty array will
* be stored instead. If a <code>null</code> value is supplied for the
* <code>contextInitializerClasses</code> an empty set will be stored instead.
* Furthermore, active profiles will be sorted, and duplicate profiles will
* be removed.
*
* @param testClass the test class for which the configuration was merged
* @param locations the merged resource locations
* @param classes the merged annotated classes
* @param contextInitializerClasses the merged context initializer classes
* @param activeProfiles the merged active bean definition profiles
* @param contextLoader the resolved <code>ContextLoader</code>
*/
public MergedContextConfiguration(
Class<?> testClass,
String[] locations,
Class<?>[] classes,
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
String[] activeProfiles, ContextLoader contextLoader) {
this.testClass = testClass;
this.locations = processLocations(locations);
this.classes = processClasses(classes);
this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses);
this.activeProfiles = processActiveProfiles(activeProfiles);
this.contextLoader = contextLoader;
}
/**
* Get the {@link Class test class} associated with this {@code MergedContextConfiguration}.
* Get the {@linkplain Class test class} associated with this {@code MergedContextConfiguration}.
*/
public Class<?> getTestClass() {
return testClass;
}
/**
* Get the merged resource locations for the {@link #getTestClass() test class}.
* Get the merged resource locations for the {@linkplain #getTestClass() test class}.
*/
public String[] getLocations() {
return locations;
}
/**
* Get the merged annotated classes for the {@link #getTestClass() test class}.
* Get the merged annotated classes for the {@linkplain #getTestClass() test class}.
*/
public Class<?>[] getClasses() {
return classes;
}
/**
* Get the merged active bean definition profiles for the {@link #getTestClass() test class}.
* Get the merged {@code ApplicationContextInitializer} classes for the
* {@linkplain #getTestClass() test class}.
*/
public Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> getContextInitializerClasses() {
return contextInitializerClasses;
}
/**
* Get the merged active bean definition profiles for the {@linkplain #getTestClass() test class}.
*/
public String[] getActiveProfiles() {
return activeProfiles;
}
/**
* Get the resolved {@link ContextLoader} for the {@link #getTestClass() test class}.
* Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}.
*/
public ContextLoader getContextLoader() {
return contextLoader;
@@ -159,7 +210,7 @@ public class MergedContextConfiguration implements Serializable {
/**
* Generate a unique hash code for all properties of this
* {@code MergedContextConfiguration} excluding the
* {@link #getTestClass() test class}.
* {@linkplain #getTestClass() test class}.
*/
@Override
public int hashCode() {
@@ -167,6 +218,7 @@ public class MergedContextConfiguration implements Serializable {
int result = 1;
result = prime * result + Arrays.hashCode(locations);
result = prime * result + Arrays.hashCode(classes);
result = prime * result + contextInitializerClasses.hashCode();
result = prime * result + Arrays.hashCode(activeProfiles);
result = prime * result + nullSafeToString(contextLoader).hashCode();
return result;
@@ -174,10 +226,11 @@ public class MergedContextConfiguration implements Serializable {
/**
* Determine if the supplied object is equal to this {@code MergedContextConfiguration}
* instance by comparing both object's {@link #getLocations() locations},
* {@link #getClasses() annotated classes}, {@link #getActiveProfiles()
* active profiles}, and the fully qualified names of their
* {@link #getContextLoader() ContextLoaders}.
* instance by comparing both object's {@linkplain #getLocations() locations},
* {@linkplain #getClasses() annotated classes},
* {@linkplain #getContextInitializerClasses() context initializer classes},
* {@linkplain #getActiveProfiles() active profiles}, and the fully qualified
* names of their {@link #getContextLoader() ContextLoaders}.
*/
@Override
public boolean equals(Object obj) {
@@ -197,6 +250,9 @@ public class MergedContextConfiguration implements Serializable {
if (!Arrays.equals(this.classes, that.classes)) {
return false;
}
if (!this.contextInitializerClasses.equals(that.contextInitializerClasses)) {
return false;
}
if (!Arrays.equals(this.activeProfiles, that.activeProfiles)) {
return false;
}
@@ -208,9 +264,10 @@ public class MergedContextConfiguration implements Serializable {
}
/**
* Provide a String representation of the {@link #getTestClass() test class},
* {@link #getLocations() locations}, {@link #getClasses() annotated classes},
* {@link #getActiveProfiles() active profiles}, and the name of the
* Provide a String representation of the {@linkplain #getTestClass() test class},
* {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes},
* {@linkplain #getContextInitializerClasses() context initializer classes},
* {@linkplain #getActiveProfiles() active profiles}, and the name of the
* {@link #getContextLoader() ContextLoader}.
*/
@Override
@@ -219,6 +276,7 @@ public class MergedContextConfiguration implements Serializable {
.append("testClass", testClass)//
.append("locations", ObjectUtils.nullSafeToString(locations))//
.append("classes", ObjectUtils.nullSafeToString(classes))//
.append("contextInitializerClasses", ObjectUtils.nullSafeToString(contextInitializerClasses))//
.append("activeProfiles", ObjectUtils.nullSafeToString(activeProfiles))//
.append("contextLoader", nullSafeToString(contextLoader))//
.toString();

View File

@@ -16,14 +16,25 @@
package org.springframework.test.context.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -66,11 +77,8 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
*
* <ul>
* <li>Creates a {@link GenericApplicationContext} instance.</li>
* <li>Sets the <em>active bean definition profiles</em> from the supplied
* <code>MergedContextConfiguration</code> in the
* {@link org.springframework.core.env.Environment Environment} of the context.</li>
* <li>Calls {@link #prepareContext(GenericApplicationContext)} to allow for customizing the context
* before bean definitions are loaded.</li>
* <li>Calls {@link #prepareContext(GenericApplicationContext, MergedContextConfiguration)}
* to allow for customizing the context before bean definitions are loaded.</li>
* <li>Calls {@link #customizeBeanFactory(DefaultListableBeanFactory)} to allow for customizing the
* context's <code>DefaultListableBeanFactory</code>.</li>
* <li>Delegates to {@link #loadBeanDefinitions(GenericApplicationContext, MergedContextConfiguration)}
@@ -95,9 +103,9 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
logger.debug(String.format("Loading ApplicationContext for merged context configuration [%s].",
mergedConfig));
}
GenericApplicationContext context = new GenericApplicationContext();
context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles());
prepareContext(context);
prepareContext(context, mergedConfig);
customizeBeanFactory(context.getDefaultListableBeanFactory());
loadBeanDefinitions(context, mergedConfig);
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
@@ -132,6 +140,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
*
* <p><b>Note</b>: this method does not provide a means to set active bean definition
* profiles for the loaded context. See {@link #loadContext(MergedContextConfiguration)}
* and {@link #prepareContext(GenericApplicationContext, MergedContextConfiguration)}
* for an alternative.
*
* @return a new application context
@@ -174,6 +183,72 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
protected void prepareContext(GenericApplicationContext context) {
}
/**
* Prepare the {@link GenericApplicationContext} created by this
* {@code SmartContextLoader} <i>before</i> bean definitions are read.
*
* <p>The default implementation:
* <ul>
* <li>Calls {@link #prepareContext(GenericApplicationContext)} for backwards
* compatibility with the {@link org.springframework.test.context.ContextLoader
* ContextLoader} SPI.</li>
* <li>Sets the <em>active bean definition profiles</em> from the supplied
* <code>MergedContextConfiguration</code> in the
* {@link org.springframework.core.env.Environment Environment} of the context.</li>
* <li>Determines what (if any) context initializer classes have been supplied
* via the {@code MergedContextConfiguration} and
* {@linkplain ApplicationContextInitializer#initialize invokes each} with the
* given application context.</li>
* </ul>
*
* <p>Any {@code ApplicationContextInitializers} implementing
* {@link org.springframework.core.Ordered Ordered} or marked with {@link
* org.springframework.core.annotation.Order @Order} will be sorted appropriately.
*
* @param applicationContext the newly created application context
* @param mergedConfig the merged context configuration
* @see ApplicationContextInitializer#initialize(GenericApplicationContext)
* @see #loadContext(MergedContextConfiguration)
* @see GenericApplicationContext#setAllowBeanDefinitionOverriding
* @see GenericApplicationContext#setResourceLoader
* @see GenericApplicationContext#setId
* @since 3.2
*/
@SuppressWarnings("unchecked")
protected void prepareContext(GenericApplicationContext applicationContext,
MergedContextConfiguration mergedConfig) {
prepareContext(applicationContext);
applicationContext.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles());
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = mergedConfig.getContextInitializerClasses();
if (initializerClasses.size() == 0) {
// no ApplicationContextInitializers have been declared -> nothing to do
return;
}
final List<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerInstances = new ArrayList<ApplicationContextInitializer<ConfigurableApplicationContext>>();
final Class<?> contextClass = applicationContext.getClass();
for (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>> initializerClass : initializerClasses) {
Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass,
ApplicationContextInitializer.class);
Assert.isAssignable(initializerContextClass, contextClass, String.format(
"Could not add context initializer [%s] since its generic parameter [%s] "
+ "is not assignable from the type of application context used by this "
+ "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(),
contextClass.getName()));
initializerInstances.add((ApplicationContextInitializer<ConfigurableApplicationContext>) BeanUtils.instantiateClass(initializerClass));
}
Collections.sort(initializerInstances, new AnnotationAwareOrderComparator());
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : initializerInstances) {
initializer.initialize(applicationContext);
}
}
/**
* Customize the internal bean factory of the ApplicationContext created by
* this <code>ContextLoader</code>.

View File

@@ -21,6 +21,7 @@ import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextConfigurationAttributes;
@@ -50,6 +51,12 @@ import org.springframework.util.ObjectUtils;
* the default loader, thus providing automatic support for either XML configuration
* files or annotated classes, but not both simultaneously.
*
* <p>As of Spring 3.2, a test class may optionally declare neither XML configuration
* files nor annotated classes and instead declare only {@linkplain
* ContextConfiguration#initializers application context initializers}. In such
* cases, an attempt will still be made to detect defaults, but their absence will
* not result an an exception.
*
* @author Sam Brannen
* @since 3.1
* @see SmartContextLoader
@@ -78,11 +85,18 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
loader.processContextConfiguration(configAttributes);
}
private static ApplicationContext delegateLoading(SmartContextLoader loader, MergedContextConfiguration mergedConfig)
throws Exception {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig));
}
return loader.loadContext(mergedConfig);
}
private static boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) {
if (loader instanceof AnnotationConfigContextLoader) {
return ObjectUtils.isEmpty(mergedConfig.getLocations()) && !ObjectUtils.isEmpty(mergedConfig.getClasses());
}
else {
} else {
return !ObjectUtils.isEmpty(mergedConfig.getLocations()) && ObjectUtils.isEmpty(mergedConfig.getClasses());
}
}
@@ -132,11 +146,9 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
// appropriate loader process the configuration.
if (configAttributes.hasLocations()) {
delegateProcessing(xmlLoader, configAttributes);
}
else if (configAttributes.hasClasses()) {
} else if (configAttributes.hasClasses()) {
delegateProcessing(annotationConfigLoader, configAttributes);
}
else {
} else {
// Else attempt to detect defaults...
// Let the XML loader process the configuration.
@@ -173,10 +185,12 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
name(annotationConfigLoader), configAttributes));
}
// If neither loader detected defaults, throw an exception.
if (!configAttributes.hasResources()) {
// If neither loader detected defaults and no initializers were declared,
// throw an exception.
if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) {
throw new IllegalStateException(String.format(
"Neither %s nor %s was able to detect defaults for context configuration %s.", name(xmlLoader),
"Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers "
+ "were declared for context configuration %s", name(xmlLoader),
name(annotationConfigLoader), configAttributes));
}
@@ -219,16 +233,19 @@ public class DelegatingSmartContextLoader implements SmartContextLoader {
List<SmartContextLoader> candidates = Arrays.asList(xmlLoader, annotationConfigLoader);
for (SmartContextLoader loader : candidates) {
// Determine if each loader can load a context from the
// mergedConfig. If it can, let it; otherwise, keep iterating.
// Determine if each loader can load a context from the mergedConfig. If it
// can, let it; otherwise, keep iterating.
if (supports(loader, mergedConfig)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig));
}
return loader.loadContext(mergedConfig);
return delegateLoading(loader, mergedConfig);
}
}
// If neither of the candidates supports the mergedConfig based on resources but
// ACIs were declared, then delegate to the ACCL.
if (!mergedConfig.getContextInitializerClasses().isEmpty()) {
return delegateLoading(annotationConfigLoader, mergedConfig);
}
throw new IllegalStateException(String.format(
"Neither %s nor %s was able to load an ApplicationContext from %s.", name(xmlLoader),
name(annotationConfigLoader), mergedConfig));