Introduce ContextCustomizer API in the TestContext Framework
Allow third-parties to contribute ContextCustomizers that can customize ApplicationContexts created by the Spring TestContext Framework (TCF) before they are refreshed. A customizer may be provided via a ContextCustomizerFactory which is registered with `spring.factories`. Each factory is consulted whenever a new ApplicationContext needs to be created by the TCF. Factories may inspect various details about the test and either return a new ContextCustomizer or null. ContextCustomizers are similar to ApplicationContextInitializers and may perform any number of tasks, including bean registration, setting of active profiles, etc. Issue: SPR-13998
This commit is contained in:
committed by
Sam Brannen
parent
406d06118d
commit
87a3a5cb3b
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* Strategy interface for customizing {@link ApplicationContext application contexts} that
|
||||
* are created and managed by the Spring TestContext Framework.
|
||||
*
|
||||
* <p>Customizers are loaded via {@link ContextCustomizerFactory} classes registered in
|
||||
* {@code spring.factories}.
|
||||
*
|
||||
* <p>Implementations should take care to implement correct {@code equals} and
|
||||
* {@code hashCode} methods since customizers form part of the
|
||||
* {@link MergedContextConfiguration} which is used as a cache key.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.3
|
||||
* @see ContextCustomizerFactory
|
||||
* @see org.springframework.test.context.support.AbstractContextLoader
|
||||
*/
|
||||
public interface ContextCustomizer {
|
||||
|
||||
/**
|
||||
* Called <i>before</i> bean definitions are read to customize the
|
||||
* {@link ConfigurableApplicationContext}.
|
||||
* @param context the context that should be prepared
|
||||
* @param mergedContextConfiguration the merged context configuration
|
||||
*/
|
||||
void customizeContext(ConfigurableApplicationContext context,
|
||||
MergedContextConfiguration mergedContextConfiguration);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* Factory registered in {@code spring.factories} that is used to create
|
||||
* {@link ContextCustomizer ContextCustomizers}. Factories are called after
|
||||
* {@link ContextLoader ContextLoaders} have been triggered but before the
|
||||
* {@link MergedContextConfiguration} is created.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.3
|
||||
*/
|
||||
public interface ContextCustomizerFactory {
|
||||
|
||||
/**
|
||||
* Get the {@link ContextCustomizer} (if any) that should be used to customize the
|
||||
* {@link ConfigurableApplicationContext} when it is created.
|
||||
* @param testClass the test class
|
||||
* @param configurationAttributes he list of context configuration attributes for the
|
||||
* test class, ordered <em>bottom-up</em> (i.e., as if we were traversing up the class
|
||||
* hierarchy); never {@code null} or empty.
|
||||
* @return a {@link ContextCustomizer} or {@code null}
|
||||
*/
|
||||
ContextCustomizer getContextCustomizer(Class<?> testClass,
|
||||
List<ContextConfigurationAttributes> configurationAttributes);
|
||||
|
||||
}
|
||||
@@ -89,6 +89,8 @@ public class MergedContextConfiguration implements Serializable {
|
||||
|
||||
private final String[] propertySourceProperties;
|
||||
|
||||
private final Set<ContextCustomizer> contextCustomizers;
|
||||
|
||||
private final ContextLoader contextLoader;
|
||||
|
||||
private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
|
||||
@@ -201,8 +203,8 @@ public class MergedContextConfiguration implements Serializable {
|
||||
public MergedContextConfiguration(MergedContextConfiguration mergedConfig) {
|
||||
this(mergedConfig.testClass, mergedConfig.locations, mergedConfig.classes,
|
||||
mergedConfig.contextInitializerClasses, mergedConfig.activeProfiles, mergedConfig.propertySourceLocations,
|
||||
mergedConfig.propertySourceProperties, mergedConfig.contextLoader,
|
||||
mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent);
|
||||
mergedConfig.propertySourceProperties, mergedConfig.contextCustomizers,
|
||||
mergedConfig.contextLoader, mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,6 +235,40 @@ public class MergedContextConfiguration implements Serializable {
|
||||
String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties,
|
||||
ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
|
||||
MergedContextConfiguration parent) {
|
||||
this(testClass, locations, classes, contextInitializerClasses, activeProfiles,
|
||||
propertySourceLocations, propertySourceProperties,
|
||||
Collections.<ContextCustomizer> emptySet(), contextLoader,
|
||||
cacheAwareContextLoaderDelegate, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code MergedContextConfiguration} instance for the
|
||||
* supplied parameters.
|
||||
* <p>If a {@code null} value is supplied for {@code locations},
|
||||
* {@code classes}, {@code activeProfiles}, {@code propertySourceLocations},
|
||||
* or {@code propertySourceProperties} an empty array will be stored instead.
|
||||
* If a {@code null} value is supplied for the
|
||||
* {@code contextInitializerClasses} 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 context resource locations
|
||||
* @param classes the merged annotated classes
|
||||
* @param contextInitializerClasses the merged context initializer classes
|
||||
* @param activeProfiles the merged active bean definition profiles
|
||||
* @param propertySourceLocations the merged {@code PropertySource} locations
|
||||
* @param propertySourceProperties the merged {@code PropertySource} properties
|
||||
* @param contextLoader the resolved {@code ContextLoader}
|
||||
* @param cacheAwareContextLoaderDelegate a cache-aware context loader
|
||||
* delegate with which to retrieve the parent context
|
||||
* @param parent the parent configuration or {@code null} if there is no parent
|
||||
* @since 4.2
|
||||
*/
|
||||
public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
|
||||
Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
|
||||
String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties,
|
||||
Set<ContextCustomizer> contextCustomizers, ContextLoader contextLoader,
|
||||
CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) {
|
||||
|
||||
this.testClass = testClass;
|
||||
this.locations = processStrings(locations);
|
||||
@@ -241,6 +277,7 @@ public class MergedContextConfiguration implements Serializable {
|
||||
this.activeProfiles = processActiveProfiles(activeProfiles);
|
||||
this.propertySourceLocations = processStrings(propertySourceLocations);
|
||||
this.propertySourceProperties = processStrings(propertySourceProperties);
|
||||
this.contextCustomizers = Collections.unmodifiableSet(contextCustomizers);
|
||||
this.contextLoader = contextLoader;
|
||||
this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate;
|
||||
this.parent = parent;
|
||||
@@ -348,6 +385,14 @@ public class MergedContextConfiguration implements Serializable {
|
||||
return this.propertySourceProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the merged {@link ContextCustomizer ContextCustomizers} that will be applied
|
||||
* when the application context is loaded.
|
||||
*/
|
||||
public Set<ContextCustomizer> getContextCustomizers() {
|
||||
return contextCustomizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}.
|
||||
*/
|
||||
@@ -424,6 +469,9 @@ public class MergedContextConfiguration implements Serializable {
|
||||
if (!Arrays.equals(this.propertySourceProperties, otherConfig.propertySourceProperties)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.contextCustomizers.equals(otherConfig.contextCustomizers)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.parent == null) {
|
||||
if (otherConfig.parent != null) {
|
||||
@@ -454,6 +502,7 @@ public class MergedContextConfiguration implements Serializable {
|
||||
result = 31 * result + Arrays.hashCode(this.activeProfiles);
|
||||
result = 31 * result + Arrays.hashCode(this.propertySourceLocations);
|
||||
result = 31 * result + Arrays.hashCode(this.propertySourceProperties);
|
||||
result = 31 * result + this.contextCustomizers.hashCode();
|
||||
result = 31 * result + (this.parent != null ? this.parent.hashCode() : 0);
|
||||
result = 31 * result + nullSafeToString(this.contextLoader).hashCode();
|
||||
return result;
|
||||
|
||||
@@ -28,11 +28,13 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizer;
|
||||
import org.springframework.test.context.ContextLoader;
|
||||
import org.springframework.test.context.MergedContextConfiguration;
|
||||
import org.springframework.test.context.SmartContextLoader;
|
||||
@@ -107,6 +109,8 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
|
||||
* {@linkplain MergedContextConfiguration#getPropertySourceProperties()
|
||||
* inlined properties} from the supplied {@code MergedContextConfiguration}
|
||||
* to the {@code Environment} of the context.</li>
|
||||
* <li>Calls any {@link MergedContextConfiguration#getContextCustomizers()
|
||||
* ContextCustomizers} that are part of the {@link MergedContextConfiguration}.</li>
|
||||
* <li>Determines what (if any) context initializer classes have been supplied
|
||||
* via the {@code MergedContextConfiguration} and instantiates and
|
||||
* {@linkplain ApplicationContextInitializer#initialize invokes} each with the
|
||||
@@ -167,6 +171,25 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the {@link ConfigurableApplicationContext} created by this
|
||||
* {@code ContextLoader} <i>after</i> bean definitions have been
|
||||
* loaded into the context but <i>before</i> the context is refreshed.
|
||||
*
|
||||
* <p>The default implementation triggers all the
|
||||
* {@link MergedContextConfiguration#getContextCustomizers() context customizers} that
|
||||
* have been registered with the {@code mergedConfig}.
|
||||
*
|
||||
* @param context the newly created application context
|
||||
* @param mergedConfig the merged context configuration
|
||||
* @since 4.3
|
||||
*/
|
||||
protected void customizeContext(GenericApplicationContext context, MergedContextConfiguration mergedConfig) {
|
||||
for (ContextCustomizer contextCustomizer : mergedConfig.getContextCustomizers()) {
|
||||
contextCustomizer.customizeContext(context, mergedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- ContextLoader -------------------------------------------------------
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte
|
||||
|
||||
// If neither of the candidates supports the mergedConfig based on resources but
|
||||
// ACIs were declared, then delegate to the annotation config loader.
|
||||
if (!mergedConfig.getContextInitializerClasses().isEmpty()) {
|
||||
if (!mergedConfig.getContextInitializerClasses().isEmpty() || !mergedConfig.getContextCustomizers().isEmpty()) {
|
||||
return delegateLoading(getAnnotationConfigLoader(), mergedConfig);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader
|
||||
loadBeanDefinitions(context, mergedConfig);
|
||||
AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
|
||||
customizeContext(context);
|
||||
customizeContext(context, mergedConfig);
|
||||
context.refresh();
|
||||
context.registerShutdownHook();
|
||||
return context;
|
||||
|
||||
@@ -38,6 +38,8 @@ import org.springframework.test.context.BootstrapContext;
|
||||
import org.springframework.test.context.CacheAwareContextLoaderDelegate;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizer;
|
||||
import org.springframework.test.context.ContextCustomizerFactory;
|
||||
import org.springframework.test.context.ContextHierarchy;
|
||||
import org.springframework.test.context.ContextLoader;
|
||||
import org.springframework.test.context.MergedContextConfiguration;
|
||||
@@ -385,10 +387,13 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
|
||||
}
|
||||
}
|
||||
|
||||
if (requireLocationsClassesOrInitializers && areAllEmpty(locations, classes, initializers)) {
|
||||
Set<ContextCustomizer> contextCustomizers = getContextCustomizers(testClass,
|
||||
Collections.unmodifiableList(configAttributesList));
|
||||
|
||||
if (requireLocationsClassesOrInitializers && areAllEmpty(locations, classes, initializers, contextCustomizers)) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"%s was unable to detect defaults, and no ApplicationContextInitializers "
|
||||
+ "were declared for context configuration attributes %s",
|
||||
+ "or ContextCustomizers were declared for context configuration attributes %s",
|
||||
contextLoader.getClass().getSimpleName(), configAttributesList));
|
||||
}
|
||||
|
||||
@@ -400,14 +405,32 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
|
||||
ActiveProfilesUtils.resolveActiveProfiles(testClass),
|
||||
mergedTestPropertySources.getLocations(),
|
||||
mergedTestPropertySources.getProperties(),
|
||||
contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
|
||||
contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig);
|
||||
|
||||
return processMergedContextConfiguration(mergedConfig);
|
||||
}
|
||||
|
||||
private Set<ContextCustomizer> getContextCustomizers(Class<?> testClass,
|
||||
List<ContextConfigurationAttributes> configurationAttributes) {
|
||||
List<ContextCustomizerFactory> factories = geContextCustomizerFactories();
|
||||
Set<ContextCustomizer> customizers = new LinkedHashSet<ContextCustomizer>(factories.size());
|
||||
for (ContextCustomizerFactory factory : factories) {
|
||||
ContextCustomizer customizer = factory.getContextCustomizer(testClass, configurationAttributes);
|
||||
if (customizer != null) {
|
||||
customizers.add(customizer);
|
||||
}
|
||||
}
|
||||
return customizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.3
|
||||
* Get the default {@link ContextCustomizerFactory} instances for this bootstrapper.
|
||||
*/
|
||||
protected List<ContextCustomizerFactory> geContextCustomizerFactories() {
|
||||
return SpringFactoriesLoader.loadFactories(ContextCustomizerFactory.class,
|
||||
getClass().getClassLoader());
|
||||
}
|
||||
|
||||
private boolean areAllEmpty(Collection<?>... collections) {
|
||||
for (Collection<?> collection : collections) {
|
||||
if (!collection.isEmpty()) {
|
||||
|
||||
@@ -256,15 +256,13 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa
|
||||
* loader <i>after</i> bean definitions have been loaded into the context but
|
||||
* <i>before</i> the context is refreshed.
|
||||
*
|
||||
* <p>The default implementation is empty but can be overridden in subclasses
|
||||
* to customize the web application context.
|
||||
*
|
||||
* @param context the newly created web application context
|
||||
* @param webMergedConfig the merged context configuration to use to load the
|
||||
* web application context
|
||||
* @see #loadContext(MergedContextConfiguration)
|
||||
*/
|
||||
protected void customizeContext(GenericWebApplicationContext context, WebMergedContextConfiguration webMergedConfig) {
|
||||
super.customizeContext(context, webMergedConfig);
|
||||
}
|
||||
|
||||
// --- ContextLoader -------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user